Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion CHANGELOG-CodeGenerators.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.4.1] - 2026-08-23

### Documentation

- Added XML documentation comments to `HalResponseGenerator`, previously undocumented. The
companion `HalResponseAttribute` in the unpublished `Chatter.Rest.Hal.Core` project was
documented too. Documentation only: no API or behavior change.

## [0.4.0] - 2026-08-22

### Added
Expand Down Expand Up @@ -74,5 +82,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- A `record` annotated with `[HalResponse]` previously generated nothing silently and now reports
`HAL0003`. Declare the type as a `partial class` to receive the HAL members.

[Unreleased]: https://github.com/brenpike/Chatter.Rest.Hal/compare/codegen/v0.4.0...HEAD
[Unreleased]: https://github.com/brenpike/Chatter.Rest.Hal/compare/codegen/v0.4.1...HEAD
[0.4.1]: https://github.com/brenpike/Chatter.Rest.Hal/compare/codegen/v0.4.0...codegen/v0.4.1
[0.4.0]: https://github.com/brenpike/Chatter.Rest.Hal/compare/codegen/v0.3.0...codegen/v0.4.0
20 changes: 19 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [2.1.1] - 2026-08-23

### Documentation

- Added XML documentation comments to the 20 public fluent builder stage interfaces under
`Builders/Stages/`, which were previously undocumented. Documentation only: no API or
behavior change.
- Enabled `GenerateDocumentationFile` for `Chatter.Rest.Hal`, so the package now ships the XML
documentation sidecar (`lib/net8.0/Chatter.Rest.Hal.xml` and
`lib/netstandard2.0/Chatter.Rest.Hal.xml`). The doc comments above previously never reached
the published package; consumers now get IntelliSense from them.
- Fixed malformed doc comments surfaced by enabling generation: closed six unclosed
`<remarks>` blocks in each of `IEmbeddedLinkObjectPropertiesSelectionStage` and
`IResourceLinkObjectPropertiesSelectionStage` (both also gained an interface summary and
documentation for their new-hiding `AsArray()` member), and replaced two unresolvable
`JsonNode.Deserialize` crefs with resolvable `JsonSerializer.Deserialize` references.

## [2.1.0] - 2026-08-23

### Fixed
Expand Down Expand Up @@ -91,6 +108,7 @@ rejected.
([#120](https://github.com/brenpike/Chatter.Rest.Hal/issues/120), open: empty-string href
tolerance).

[Unreleased]: https://github.com/brenpike/Chatter.Rest.Hal/compare/hal/v2.1.0...HEAD
[Unreleased]: https://github.com/brenpike/Chatter.Rest.Hal/compare/hal/v2.1.1...HEAD
[2.1.1]: https://github.com/brenpike/Chatter.Rest.Hal/compare/hal/v2.1.0...hal/v2.1.1
[2.1.0]: https://github.com/brenpike/Chatter.Rest.Hal/compare/hal/v2.0.0...hal/v2.1.0
[2.0.0]: https://github.com/brenpike/Chatter.Rest.Hal/compare/hal/v1.1.0...hal/v2.0.0
8 changes: 4 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ Chatter.Rest.Hal is a .NET/C# implementation of the HAL (Hypertext Application L
| `docs/usage.md` | Copy-paste examples for building, serializing, deserializing |
| `docs/development.md` | Build/test/pack commands, code style, test conventions, CI/CD |
| `docs/HAL_TEST_PLAN.md` | HAL spec-to-test mapping; consult for spec compliance and tests |
| `docs/aspnetcore/requirements.md` | Package requirements for `Chatter.Rest.Hal.AspNetCore` |
| `docs/aspnetcore/architecture.md` | Architecture, pseudocode, and test strategy for `Chatter.Rest.Hal.AspNetCore` |
| `docs/aspnetcore/requirements.md` | Package requirements for `Chatter.Rest.Hal.AspNetCore` (planned package — design docs) |
| `docs/aspnetcore/architecture.md` | Architecture, pseudocode, and test strategy for `Chatter.Rest.Hal.AspNetCore` (planned package — design docs) |

## Solution Structure

Expand Down Expand Up @@ -81,8 +81,8 @@ Observed conventions:

| Package | Version |
|---|---|
| `Chatter.Rest.Hal` | `2.1.0` |
| `Chatter.Rest.Hal.CodeGenerators` | `0.4.0` |
| `Chatter.Rest.Hal` | `2.1.1` |
| `Chatter.Rest.Hal.CodeGenerators` | `0.4.1` |

External dependency: `Chatter.Rest.UriTemplates` v0.1.0.

Expand Down
61 changes: 35 additions & 26 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ Minimal, copy-paste example (build, serialize):
using System;
using System.Text.Json;
using Chatter.Rest.Hal;
using Chatter.Rest.Hal.Builders;

// Build a simple resource with state and a self link
var resource = ResourceBuilder
Expand All @@ -96,6 +97,8 @@ Console.WriteLine(json);
*/
```

> **Note:** `ResourceBuilder` lives in the `Chatter.Rest.Hal.Builders` namespace. Later snippets in this README omit the `using` directives for brevity; each builder example assumes `using Chatter.Rest.Hal;` and `using Chatter.Rest.Hal.Builders;`.

If you want compile-time helpers generated for your response types, also install the code generators package:

```bash
Expand Down Expand Up @@ -276,30 +279,38 @@ var resource = ResourceBuilder.WithState(new { currentlyProcessing = 14, shipped
"self": {
"href": "/orders"
},
"curies": {
"href": "http://example.com/docs/rels/{rel}",
"templated": true,
"name": "ea"
},
"curies": [
{
"href": "http://example.com/docs/rels/{rel}",
"templated": true,
"name": "ea"
}
],
"next": {
"href": "/orders?page=2"
},
"ea:find": {
"href": "/orders{?id}",
"templated": true
},
"ea:admin": {
"href": "/admins/2",
"title": "Fred"
}
"ea:admin": [
{
"href": "/admins/2",
"title": "Fred"
},
{
"href": "/admins/5",
"title": "Kate"
}
]
},
"_embedded": {
"ea:order": [
{
"total": 10,
"currency": "USD",
"status": "shipped",
"id": "6d5edc98-8b81-435f-ad7a-a66a60d91bd2",
"Id": "6d5edc98-8b81-435f-ad7a-a66a60d91bd2",
"Total": 10,
"Currency": "USD",
"Status": "shipped",
"_links": {
"self": {
"href": "/orders/6d5edc98-8b81-435f-ad7a-a66a60d91bd2"
Expand All @@ -315,10 +326,10 @@ var resource = ResourceBuilder.WithState(new { currentlyProcessing = 14, shipped
}
},
{
"total": 20,
"currency": "CAD",
"status": "processing",
"id": "418845a7-ec41-4288-83eb-22a8bb22e472",
"Id": "418845a7-ec41-4288-83eb-22a8bb22e472",
"Total": 20,
"Currency": "CAD",
"Status": "processing",
"_links": {
"self": {
"href": "/orders/418845a7-ec41-4288-83eb-22a8bb22e472"
Expand All @@ -338,7 +349,7 @@ var resource = ResourceBuilder.WithState(new { currentlyProcessing = 14, shipped
}
```

> The output above shows two of the six orders. Remaining orders follow the same structure.
> The output above shows two of the six orders. Remaining orders follow the same structure. `Order` property names serialize with their C# casing (`Id`, `Total`, ...) because the example does not configure a JSON naming policy.

---

Expand Down Expand Up @@ -468,7 +479,7 @@ var resources = resource!.GetResourceCollection("ea:order");

### Get a Link by relation

Retrieve a single link by its relation. Returns `null` if not found, or throws an exception if multiple links with the same relation exist:
Retrieve a single link by its relation. Returns `null` if not found. The lookup uses `SingleOrDefault`, so it throws `InvalidOperationException` if multiple links share the same relation — though since 2.0.0 duplicate relations cannot arise: `LinkCollection.Add` rejects a duplicate relation key, and deserialization normalizes duplicate relations last-wins before they reach the collection.

```csharp
var link = resource!.GetLinkOrDefault("self");
Expand All @@ -492,7 +503,7 @@ var linkObj = resource!.GetLinkObjectOrDefault("curies", "ea");

### Get a Link Object of a Link by relation only

For relations with a single link object, retrieve it directly by relation:
Retrieve the first link object for a relation. If the relation holds several link objects, this returns the **first** one (it uses `FirstOrDefault`) rather than throwing:

```csharp
var linkObj = resource!.GetLinkObjectOrDefault("self");
Expand All @@ -519,7 +530,6 @@ To force **all** link relations to serialize as JSON arrays regardless of count,

```csharp
using Chatter.Rest.Hal;
using Chatter.Rest.Hal.Extensions;

// ASP.NET Core
services.AddControllers().AddJsonOptions(o =>
Expand Down Expand Up @@ -550,11 +560,10 @@ To force array representation for a **specific relation** only, use `AsArray()`

```csharp
var resource = ResourceBuilder.WithState(new { total = 5 })
.AddLinks()
.AddLink("orders").AsArray() // always emit as array
.AddLinkObject("/orders/1")
.AddSelf() // count-based (default behavior)
.AddLinkObject("/api/orders")
.AddLink("orders").AsArray() // always emit as array
.AddLinkObject("/orders/1")
.AddSelf() // count-based (default behavior)
.AddLinkObject("/api/orders")
.Build();
```

Expand Down
33 changes: 26 additions & 7 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,13 +164,15 @@ public interface IAddResourceStage
}
```

> **Return-target semantics.** `AddResource()` / `AddResource(state)` return the newly added embedded resource — chained link/curies/embed calls configure that resource. `AddResources<T>()` returns the collection builder instead: link/curies/embed calls chained on its return value target the resource that **owns** the `_embedded` entry, not each added item. Per-item links, curies, and nested embeds must be configured inside the `builder` callback.

---

## 7. `IEmbeddedResourceCreationStage`

Namespace: `Chatter.Rest.Hal.Builders.Stages.Embedded`

The embedded-context mirror of `IResourceCreationStage`. Returned by `AddResource()` / `AddResources()`. Supports the same link, curies, and embedded operations as the root resource stage, plus `Build()` which terminates the entire chain back to the root `Resource`.
The embedded-context mirror of `IResourceCreationStage`, returned by the `IAddResourceStage` methods. It exposes the same link, curies, and embedded operations as the root resource stage, plus `Build()` which terminates the entire chain back to the root `Resource` — but what those operations target depends on which method returned the stage. After `AddResource()` / `AddResource(state)` they configure the newly added embedded resource; after `AddResources<T>()` the stage is the collection builder, so link/curies/embed calls target the resource that owns the `_embedded` entry and per-item configuration happens only inside the `AddResources` callback (see the return-target semantics note in Section 6).

```csharp
public interface IEmbeddedResourceCreationStage :
Expand Down Expand Up @@ -313,7 +315,9 @@ Extension methods on `Resource` for navigating links and embedded resources.
```csharp
public static class ResourceExtensions
{
// Find a link by relation. Returns null if not found.
// Find a link by relation. Returns null if not found. Implemented with
// SingleOrDefault: throws InvalidOperationException when multiple links
// share the same relation.
public static Link? GetLinkOrDefault(this Resource resource, string relation);

// Get all link objects for a relation. Returns null if the relation is not found.
Expand Down Expand Up @@ -345,22 +349,31 @@ Extension methods on `LinkCollection`.
```csharp
public static class LinkCollectionExtensions
{
// Find a link by relation. Returns null if not found.
// Find a link by relation. Returns null if not found. Implemented with
// SingleOrDefault: throws InvalidOperationException when multiple links
// share the same relation.
public static Link? GetLinkOrDefault(this LinkCollection links, string relation);

// Get all link objects for a relation. Returns null if the relation is not found.
public static LinkObjectCollection? GetLinkObjects(this LinkCollection links, string relation);

// Get the first link object for a relation. Returns null if not found.
// Implemented with FirstOrDefault: a relation may legitimately carry
// several link objects, so this returns the first rather than throwing.
public static LinkObject? GetLinkObjectOrDefault(this LinkCollection links, string linkRelation);

// Get a named link object within a relation. Returns null if not found.
public static LinkObject? GetLinkObjectOrDefault(this LinkCollection links, string linkRelation, string linkObjectName);

// Expand a CURIE relation (e.g. "acme:widgets") to its full URI using the
// "curies" link relation defined in this collection. Returns the original
// relation unchanged if no matching CURIE definition is found, the relation
// contains no colon, or the CURIE template lacks the {rel} token.
// relation unchanged when any of the following holds:
// - the relation contains no colon, or the colon is the first character;
// - the reference after the colon is empty (e.g. "acme:");
// - no "curies" link relation exists, or no CURIE definition matches the
// prefix;
// - the matching CURIE definition is not marked "templated": true;
// - the CURIE template lacks the {rel} token.
// Returns an empty string when relation is null or empty.
public static string ExpandCurieRelation(this LinkCollection links, string relation);
}
Expand All @@ -376,6 +389,12 @@ Namespace: `Chatter.Rest.Hal`
public static class LinkObjectCollectionExtensions
{
// Get a link object by its name property. Returns null if not found.
// Implemented with SingleOrDefault: throws InvalidOperationException when
// multiple link objects in the collection share the same name. Note the
// asymmetry with the by-relation overload
// GetLinkObjectOrDefault(LinkCollection, string linkRelation) in §11, which
// uses FirstOrDefault and never throws when a relation carries several
// link objects.
public static LinkObject? GetLinkObjectOrDefault(this LinkObjectCollection linkObjects, string name);
}

Expand Down Expand Up @@ -435,7 +454,7 @@ public sealed class HalJsonOptions

### `JsonSerializerOptionsExtensions`

Namespace: `Chatter.Rest.Hal.Extensions`
Namespace: `Chatter.Rest.Hal`

```csharp
public static class JsonSerializerOptionsExtensions
Expand Down Expand Up @@ -498,7 +517,7 @@ var uri = link!.Expand(("q", "dotnet"), ("lang", "en"));

### Standalone `UriTemplate` Class

The `UriTemplate` class in `Chatter.Rest.UriTemplates` can be used independently of the HAL library for RFC 6570 Levels 1-3 template expansion. See [docs/uri-templates/usage.md](uri-templates/usage.md) for the full API reference and operator examples.
The `UriTemplate` class in `Chatter.Rest.UriTemplates` can be used independently of the HAL library for RFC 6570 Levels 1-3 template expansion. `Chatter.Rest.UriTemplates` is an external NuGet package dependency; see the [Chatter.Rest.UriTemplates package on NuGet](https://www.nuget.org/packages/Chatter.Rest.UriTemplates) for its API reference and operator examples.

---

Expand Down
Loading
Loading