From c00741fdb8e713245a489f88b768cb4f7cd911cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martina=20Kubinov=C3=A1?= Date: Wed, 17 Jun 2026 11:07:09 +0200 Subject: [PATCH] Add skills for AI agents --- .agents/skills/fjson-api/SKILL.md | 61 +++++++ .../fjson-api/references/anti-patterns.md | 69 ++++++++ .../skills/fjson-api/references/examples.md | 161 ++++++++++++++++++ .../references/preferred-patterns.md | 46 +++++ .claude/skills | 1 + AGENTS.md | 2 + CLAUDE.md | 2 + 7 files changed, 342 insertions(+) create mode 100644 .agents/skills/fjson-api/SKILL.md create mode 100644 .agents/skills/fjson-api/references/anti-patterns.md create mode 100644 .agents/skills/fjson-api/references/examples.md create mode 100644 .agents/skills/fjson-api/references/preferred-patterns.md create mode 120000 .claude/skills create mode 100644 CLAUDE.md diff --git a/.agents/skills/fjson-api/SKILL.md b/.agents/skills/fjson-api/SKILL.md new file mode 100644 index 0000000..a2d0718 --- /dev/null +++ b/.agents/skills/fjson-api/SKILL.md @@ -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), 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`. +- `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`. diff --git a/.agents/skills/fjson-api/references/anti-patterns.md b/.agents/skills/fjson-api/references/anti-patterns.md new file mode 100644 index 0000000..0933cc2 --- /dev/null +++ b/.agents/skills/fjson-api/references/anti-patterns.md @@ -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`; 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. diff --git a/.agents/skills/fjson-api/references/examples.md b/.agents/skills/fjson-api/references/examples.md new file mode 100644 index 0000000..0542cf1 --- /dev/null +++ b/.agents/skills/fjson-api/references/examples.md @@ -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 + +[] +type Note = { + Value: string + Language: string +} + +[] +type NoteData = { Type: string; Attributes: Note } + +[] +type NoteEnvelope = { Data: NoteData } + +[] +let roundTrip = + testCase "POST round-trips the JSON:API envelope" <| fun _ -> + use webServer = WebServer.start 9990 { + Get = [] + Post = [ + route "/notes" + >=> mustAccept [ JsonApi.ContentType ] + >=> bindJson (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 +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 +``` diff --git a/.agents/skills/fjson-api/references/preferred-patterns.md b/.agents/skills/fjson-api/references/preferred-patterns.md new file mode 100644 index 0000000..ae7e7ae --- /dev/null +++ b/.agents/skills/fjson-api/references/preferred-patterns.md @@ -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`, 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, 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. diff --git a/.claude/skills b/.claude/skills new file mode 120000 index 0000000..2b7a412 --- /dev/null +++ b/.claude/skills @@ -0,0 +1 @@ +../.agents/skills \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md index 69296fb..5e5c214 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..29dede4 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,2 @@ + +@AGENTS.md