Skip to content
Open
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
61 changes: 61 additions & 0 deletions .agents/skills/fjson-api/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
---
name: fjson-api
description: Use whenever generating or reviewing F# code that builds, parses, or sends JSON:API requests/responses with Alma.JsonApi — calls to JsonApiRequest.create or JsonApiRequest.parse, Http.get/Http.post (and the *WithHeaders variants returning AsyncResult<string, JsonApiHttpError>), constructs JsonApiErrorDto (badRequest/notFound/conflict) or JsonApiErrorResponseData, matches on ResponseError, or wires Giraffe tracing via Trace.Http.start, Trace.Http.active and Trace.Http.finishTraceHandler. Trigger also on mentions of JSON:API, application/vnd.api+json, Felicity, HttpScopedTrace, resource type vs collection naming, or Path/Api/Url request composition.
---

# F-Json-Api

Library: [alma-oss/fjson-api](https://github.com/alma-oss/fjson-api)
NuGet: `Alma.JsonApi`

## Purpose

`Alma.JsonApi` provides JSON:API request/response types, a typed request parser, an `AsyncResult`-based HTTP client (GET/POST with trace propagation), and Giraffe tracing middleware for Felicity JSON:API servers. It standardizes the `application/vnd.api+json` content type, error DTOs, and resource naming across services that expose or consume JSON:API endpoints.

## When to Use

- Building or parsing JSON:API request bodies (`{ data: { type, attributes } }`).
- Calling a JSON:API endpoint and handling typed success/error results.
- Producing JSON:API error payloads with correct status/title/detail.
- Adding distributed tracing to a Giraffe/Felicity JSON:API pipeline.

## When NOT to Use

- Non-JSON:API HTTP calls — use a plain HTTP client instead.
- Plain JSON serialization unrelated to the JSON:API envelope.
- Tracing outside an ASP.NET Core / Giraffe `HttpContext` (the tracing module depends on Giraffe).

## Main Concepts

- `JsonApi.ContentType` — the `application/vnd.api+json` media type literal.
- `JsonApiData<'Attributes>` — the `{ Type; Attributes }` resource object inside a request.
- `JsonApiRequest<'Attributes>` — the `{ Data }` envelope; built with `JsonApiRequest.create`, read with `JsonApiRequest.parse`.
- `JsonApiRequestParseError<'Error>` — `InvalidRequest of exn | InvalidRequestData of 'Error`, the failure DU from parsing.
- `JsonApiErrorDto` — a single error `{ Status; Title; Detail }` with `badRequest`/`notFound`/`conflict` factories.
- `JsonApiErrorResponseData` — `{ Errors }` wrapper built from `ofError`/`ofErrors`.
- `JsonApiResource.Type` (`ResourceType`) — resource type, camelCase plural, hyphen for subresources.
- `JsonApiResource.Collection` (`CollectionName`) — collection name, lowercase plural with hyphens.
- `Url` / `Api` / `Path` — request addressing; `Path = Api -> Url`, with `Path.id` and `Url.asUri`.
- `Http.get` / `Http.post` (+ `*WithHeaders`) — JSON:API calls returning `AsyncResult<string, JsonApiHttpError>`.
- `JsonApiHttpError` — `ApiError | ApiErrorMessage | ResponseError | GenericResponseError`, the HTTP failure DU.
- `ResponseError` — captures request URI, status code, method, and response body of a 4xx/5xx.
- `HttpScopedTrace` — a trace scoped to an `HttpContext`; managed via the `Trace.Http` helpers.
- `Trace.Http.start` / `Trace.Http.active` / `Trace.Http.finishTraceHandler` — start, retrieve, and finish a request-scoped trace.

## Related Libraries

- `Alma.Tracing` — span model, B3 header propagation, `Trace.ChildOf` child spans.
- `Alma.Serializer` — `Serialize.toJson` used to serialize request bodies.
- `Feather.ErrorHandling` — `asyncResult`/`result` computation expressions and `AsyncResult` combinators.
- `Giraffe` — `HttpHandler` pipeline the tracing middleware plugs into.
- `FSharp.Data` — `JsonProvider` backing `JsonApiRequest.parse`.

## Keywords for Search

JSON:API, application/vnd.api+json, Alma.JsonApi, JsonApiRequest, create, parse, JsonApiData, JsonApiErrorDto, badRequest, notFound, conflict, JsonApiErrorResponseData, JsonApiResource, ResourceType, CollectionName, Http.get, Http.post, getWithHeaders, postWithHeaders, AsyncResult, JsonApiHttpError, ResponseError, Url, Api, Path, HttpScopedTrace, Trace.Http.start, finishTraceHandler, Trace.ChildOf, Giraffe, Felicity, FSharp.Data JsonProvider, Alma.Serializer, Feather.ErrorHandling

## Reference Files

- For composition principles, recommended API usage, error handling, integration, naming, and testing guidance, read `references/preferred-patterns.md`.
- For known pitfalls, incorrect assumptions, and legacy usage, read `references/anti-patterns.md`.
- For worked, self-contained code examples ordered by complexity, read `references/examples.md`.
69 changes: 69 additions & 0 deletions .agents/skills/fjson-api/references/anti-patterns.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# Anti-Patterns

Each entry is **mistake → why → fix**.

## Hardcoding the content type

- **Mistake**: writing the string `"application/vnd.api+json"` directly in headers or `Accept`.
- **Why**: duplicates a value the library already owns; a typo silently breaks content negotiation.
- **Fix**: use the `JsonApi.ContentType` literal.

## Treating an HTTP call as if it returns a plain string

- **Mistake**: using the result of `Http.get`/`Http.post` as the response body without handling the `Error` case.
- **Why**: the return type is `AsyncResult<string, JsonApiHttpError>`; a 4xx/5xx or transport failure produces `Error`, not an exception you can ignore.
- **Fix**: bind inside the `asyncResult` CE or match `Ok`/`Error`, and pattern-match `JsonApiHttpError` on failure.

## Assuming every failure is an HTTP status error

- **Mistake**: matching only `JsonApiHttpError.ResponseError` and discarding the other cases.
- **Why**: transport exceptions surface as `ApiError`/`GenericResponseError` and plain failures as `ApiErrorMessage`; only `ResponseError` carries a status code.
- **Fix**: handle all four `JsonApiHttpError` cases; read status/body only from `ResponseError`.

## Expecting request content on GET errors

- **Mistake**: assuming `ResponseError.requestContent` always returns a body.
- **Why**: it is `Some` only for POST requests and `None` for GET.
- **Fix**: treat `requestContent` as optional and only rely on it for POST.

## Sending raw attributes instead of the JSON:API envelope

- **Mistake**: posting an attributes record (or a hand-built JSON string) directly.
- **Why**: JSON:API servers expect `{ data: { type, attributes } }`; a bare object is rejected.
- **Fix**: wrap with `JsonApiRequest.create` and let `Http.post` serialize it.

## Pre-serializing the POST body

- **Mistake**: calling `Serialize.toJson` (or another serializer) yourself and passing the string to the client.
- **Why**: `Http.post` already serializes the `JsonApiRequest` internally; double-serializing produces an escaped string body.
- **Fix**: pass the `JsonApiRequest<'T>` value; let the client serialize it.

## Setting trace headers or `http.*` tags manually

- **Mistake**: adding B3 headers or `http.method`/`http.url`/`http.status_code` tags yourself around a call.
- **Why**: the client and `Trace.Http` middleware inject propagation headers and set these tags automatically; manual ones collide or duplicate.
- **Fix**: rely on the built-in injection; add only your own domain-neutral child spans via `Trace.ChildOf.start`.

## Using `finishTraceHandler` without an active scoped trace

- **Mistake**: appending `Trace.Http.finishTraceHandler` to the pipeline but never calling `Trace.Http.start` to create the `HttpScopedTrace`.
- **Why**: the finish handler tags `http.status_code` on the request-scoped trace; with nothing started there is no span to finish.
- **Fix**: start the trace at the operation entry with `Trace.Http.start name ctx`, then let `finishTraceHandler` close it.

## Assuming a pooled / reused HttpClient

- **Mistake**: relying on connection reuse across many high-frequency calls.
- **Why**: each `Http.get`/`Http.post` constructs a new `HttpClient` per request.
- **Fix**: for high-throughput paths, batch work or add pooling at a higher layer rather than expecting the client to reuse sockets.

## Misnaming resource types and collections

- **Mistake**: using snake_case, singular, or PascalCase names, or swapping type and collection conventions.
- **Why**: `ResourceType` must be camelCase plural (hyphen for subresources) and `CollectionName` lowercase hyphenated; inconsistent names break routing/contract expectations.
- **Fix**: follow the naming convention and unwrap with the `value` accessors.

## Editing generated or cached files

- **Mistake**: hand-editing `src/schema/request.json` without rebuilding, or modifying files under `packages/`.
- **Why**: the schema feeds the compile-time `JsonProvider`, so stale builds give wrong inferred types; `packages/` is Paket-managed.
- **Fix**: rebuild after any schema change and let Paket manage cached packages.
161 changes: 161 additions & 0 deletions .agents/skills/fjson-api/references/examples.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
# Examples

All example code for this skill lives here. Examples are ordered from simplest to most complete and use neutral placeholder names only.

## Basic Example — build and POST a request

```fsharp
open Alma.JsonApi

type WidgetAttributes = {
Name: string
Size: int
}

let api = Api "https://example-api.test"
let createPath: Path = fun (Api root) -> Url $"{root}/widgets"

let send () =
{ Name = "demo"; Size = 3 }
|> JsonApiRequest.create "widgets"
|> Http.post createPath api
```

## Realistic Example — GET and handle typed errors

```fsharp
open Alma.JsonApi
open System.Net
open Feather.ErrorHandling

let api = Api "https://example-api.test"
let getPath: Path = fun (Api root) -> Url $"{root}/widgets/42"

let fetch () = asyncResult {
let! body = Http.get getPath api
return body
}

let describeFailure (error: JsonApiHttpError) =
match error with
| JsonApiHttpError.ResponseError responseError ->
let code = responseError |> ResponseError.statusCode
let uri = responseError |> ResponseError.requestUri
let payload = responseError |> ResponseError.responseContent
sprintf "HTTP %A at %A: %s" code uri payload
| JsonApiHttpError.ApiErrorMessage message -> message
| other -> other |> JsonApiHttpError.format

let run () =
match fetch () |> Async.RunSynchronously with
| Ok body -> printfn "OK: %s" body
| Error error -> error |> describeFailure |> printfn "Failed: %s"
```

## Integration Example — Giraffe pipeline with request-scoped tracing

```fsharp
open Giraffe
open Alma.Tracing
open Alma.Tracing.Extension.Giraffe

// Append finishTraceHandler so the final http.status_code is tagged on the request trace.
let webApp =
choose [
Trace.Http.finishTraceHandler
>=> route "/widgets" >=> Successful.OK "ok"
]

// Inside a Felicity operation: start the request trace, then add child spans for sub-work.
let handleCreate (ctx: Microsoft.AspNetCore.Http.HttpContext) =
let requestTrace = ctx |> Trace.Http.start "Create Widget"
use _ = "Validate input" |> Trace.ChildOf.start requestTrace.Trace

// ... do work ...

let active = Trace.Http.active ctx
use _ = "Persist widget" |> Trace.ChildOf.start active
()
```

## Test Example — Expecto round-trip against an in-process server

```fsharp
open Expecto
open Giraffe
open Alma.JsonApi
open JsonApi.TestUtils

[<CLIMutable>]
type Note = {
Value: string
Language: string
}

[<CLIMutable>]
type NoteData = { Type: string; Attributes: Note }

[<CLIMutable>]
type NoteEnvelope = { Data: NoteData }

[<Tests>]
let roundTrip =
testCase "POST round-trips the JSON:API envelope" <| fun _ ->
use webServer = WebServer.start 9990 {
Get = []
Post = [
route "/notes"
>=> mustAccept [ JsonApi.ContentType ]
>=> bindJson<NoteEnvelope> (fun { Data = { Attributes = note } } ->
Successful.OK note
)
]
}
webServer.Run()

let post: Path = fun (Api root) -> Url $"{root}/notes"

let response =
{ Value = "ahoj světe"; Language = "cs" }
|> JsonApiRequest.create "notes"
|> Http.post post webServer.Api
|> Async.RunSynchronously

Expect.isOk response "Response should be OK for a 2xx round-trip"
```

## Full Workflow — parse an incoming request, build an error response

```fsharp
open Alma.JsonApi
open FSharp.Data
open Feather.ErrorHandling

type WidgetAttributes = {
Name: string
Size: int
}

// Decoder: JsonValue -> Result<WidgetAttributes, string>
let parseWidget (json: JsonValue) =
match json.TryGetProperty "name", json.TryGetProperty "size" with
| Some name, Some size ->
Ok { Name = name.AsString(); Size = size.AsInteger() }
| _ -> Error "Missing required attributes"

let handle (rawBody: string) =
match JsonApiRequest.parse parseWidget rawBody with
| Ok request ->
// request.Data.Type and request.Data.Attributes are now typed
Ok request.Data.Attributes
| Error (JsonApiRequestParseError.InvalidRequestData detail) ->
detail
|> JsonApiErrorDto.badRequest
|> JsonApiErrorResponseData.ofError
|> Error
| Error (JsonApiRequestParseError.InvalidRequest _) ->
"Malformed JSON:API request"
|> JsonApiErrorDto.badRequest
|> JsonApiErrorResponseData.ofError
|> Error
```
46 changes: 46 additions & 0 deletions .agents/skills/fjson-api/references/preferred-patterns.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Preferred Patterns

## Core Principles

- Always go through the JSON:API envelope: a request is `{ data: { type, attributes } }`, never a bare attributes object.
- Treat every HTTP call as fallible: the client returns `AsyncResult<string, JsonApiHttpError>`, so branch on both `Ok` and `Error` paths.
- Use the provided content-type literal `JsonApi.ContentType` instead of typing `application/vnd.api+json` by hand.
- Keep request addressing as data: an `Api` plus a `Path` (`Api -> Url`) function, so the same `Api` can be reused across endpoints.
- Let the library own tracing tags and header propagation; do not set `http.*` tags or B3 headers manually.

## Recommended API Usage

- **Build a request**: `JsonApiRequest.create dataType attributes` produces a `JsonApiRequest<'Attributes>`. The first argument is the resource `type` string; the second is your attributes record.
- **Parse a request**: `JsonApiRequest.parse parseData request` takes a `JsonValue -> Result<'Attributes, 'Error>` decoder and a raw JSON string, returning `Result<JsonApiRequest<'Attributes>, JsonApiRequestParseError<'Error>>`. Parse failures surface as `InvalidRequest`; decoder failures as `InvalidRequestData`. See `examples.md` → Full Workflow.
- **Send a request**: `Http.get path api` and `Http.post path api request` cover the common case; `Http.getWithHeaders`/`Http.postWithHeaders` add a `(string * string) list` of extra headers. See `examples.md` → Basic Example and Realistic Example.
- **Address an endpoint**: define a `Path` as `fun (Api api) -> Url $"{api}/segment"`, or use `Path.id` to use the `Api` value unchanged. Convert with `Url.asUri` when you need a `System.Uri`.
- **Build error payloads**: `JsonApiErrorDto.badRequest`/`notFound`/`conflict` create a single DTO; wrap one or many with `JsonApiErrorResponseData.ofError`/`ofErrors`.

## Error Handling

- Compose calls inside the `asyncResult` computation expression from `Feather.ErrorHandling`; `let!` short-circuits on the first `Error`.
- Pattern-match `JsonApiHttpError` to distinguish a real HTTP failure (`ResponseError`, carrying URI, status code, request method, and body) from transport/exception failures (`ApiError`, `GenericResponseError`) and plain messages (`ApiErrorMessage`).
- Read details off a `ResponseError` with `ResponseError.statusCode`, `requestUri`, `responseContent`, and `requestContent` (which is `Some` only for POST). Use `JsonApiHttpError.format` or `ResponseError.format` for diagnostics. See `examples.md` → Realistic Example.

## Composition

- Because `Path` is a function `Api -> Url`, build parameterized paths by returning a `Path` from a function that closes over route arguments, then apply the shared `Api`.
- Chain dependent calls with the `asyncResult` CE rather than nesting `Async.RunSynchronously`.

## Integration with Other Libraries

- **Alma.Serializer**: POST bodies are serialized internally with `Serialize.toJson`; ensure attribute records are serializer-friendly rather than serializing manually before calling `Http.post`.
- **Alma.Tracing**: the client continues the current active trace (`Trace.Active.current`) and injects B3 headers automatically; create sub-spans for your own work with `Trace.ChildOf.start`.
- **Giraffe / Felicity tracing**: start a request-scoped trace with `Trace.Http.start name ctx`, retrieve it later with `Trace.Http.active ctx`, and append `Trace.Http.finishTraceHandler` in the pipeline so the final `http.status_code` is tagged. Starting a new HTTP trace finishes any previously active one first. See `examples.md` → Integration Example.
- **FSharp.Data**: `JsonApiRequest.parse` is backed by a `JsonProvider`; your `parseData` decoder typically uses a `JsonProvider`-generated type for the attributes.

## Naming Conventions

- Resource `Type` (`ResourceType`): plural, camelCase; a subresource is appended after a hyphen (e.g. `widgetItems-state`).
- Collection (`CollectionName`): plural, lowercase, hyphen-separated (e.g. `widget-items`).
- Unwrap either with its `value` accessor (`Type.value`, `CollectionName.value`).

## Testing Recommendations

- Exercise the client against a real in-process Giraffe server bound to a test port; assert on `Ok`/`Error` and, for failures, destructure `JsonApiHttpError.ResponseError` to check URI, status code, and body.
- Cover round-tripping (POST a request, decode the echoed response) including non-ASCII payloads to catch encoding regressions. See `examples.md` → Test Example.
1 change: 1 addition & 0 deletions .claude/skills
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# AGENTS.md — Alma.JsonApi (fjson-api)

This repo ships Agent Skill for the `Alma.JsonApi` library. Compatible agents discover it automatically; see `.agents/skills/fjson-api/SKILL.md`.

## Project Purpose

F# library (`Alma.JsonApi`) providing JSON:API types, request/response parsing, HTTP client helpers (GET/POST with tracing), and Giraffe-based tracing middleware for Felicity JSON:API servers. Used by Alma microservices that expose or consume JSON:API endpoints. Published as a NuGet package.
Expand Down
2 changes: 2 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<!-- Imports repo-level agent guidance; Claude Code does not read AGENTS.md natively. -->
@AGENTS.md