Skip to content

Commit d9304e6

Browse files
committed
docs: split contract pattern guides
1 parent 1c7c1d2 commit d9304e6

10 files changed

Lines changed: 302 additions & 276 deletions

README.md

Lines changed: 35 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,17 @@
1414
[![Last Commit](https://img.shields.io/github/last-commit/adz/CodecMapper)](https://github.com/adz/CodecMapper/commits/main)
1515
[![Stars](https://img.shields.io/github/stars/adz/CodecMapper?style=social)](https://github.com/adz/CodecMapper/stargazers)
1616

17-
`CodecMapper` is a schema-first serialization library for F# focused on explicit wire contracts, symmetric encode/decode behavior, and portability to Native AOT and Fable-style targets.
17+
CodecMapper is a schema-first serialization library for F# focused on explicit wire contracts,
18+
symmetric encode/decode behavior, and portability to Native AOT and Fable-style targets.
1819

19-
It is for cases where serializer attributes and implicit conventions stop being helpful. You define one schema that mirrors the wire shape, then compile it into reusable codecs.
20+
It's for cases where serializer attributes and implicit conventions stop being helpful.
21+
You define one schema that mirrors the wire shape, then compile it into reusable codecs.
2022

2123
## Why the schema feels different
2224

2325
```fsharp
2426
open CodecMapper
27+
open CodecMapper.Schema
2528
2629
type Address = { Street: string; City: string }
2730
let makeAddress street city = { Street = street; City = city }
@@ -30,19 +33,19 @@ type Person = { Id: int; Name: string; Home: Address }
3033
let makePerson id name home = { Id = id; Name = name; Home = home }
3134
3235
let addressSchema =
33-
Schema.define<Address>
34-
|> Schema.construct makeAddress
35-
|> Schema.field "street" _.Street
36-
|> Schema.field "city" _.City
37-
|> Schema.build
36+
define<Address>
37+
|> construct makeAddress
38+
|> field "street" _.Street
39+
|> field "city" _.City
40+
|> build
3841
3942
let personSchema =
40-
Schema.define<Person>
41-
|> Schema.construct makePerson
42-
|> Schema.field "id" _.Id
43-
|> Schema.field "name" _.Name
44-
|> Schema.fieldWith "home" _.Home addressSchema
45-
|> Schema.build
43+
define<Person>
44+
|> construct makePerson
45+
|> field "id" _.Id
46+
|> field "name" _.Name
47+
|> fieldWith "home" _.Home addressSchema
48+
|> build
4649
4750
let codec = Json.compile personSchema
4851
let person =
@@ -142,6 +145,8 @@ One of the main benefits over convention-based serializers is that model evoluti
142145
If your domain gets richer but the wire contract does not need to change yet, keep the same wire shape and refine it:
143146

144147
```fsharp
148+
open CodecMapper.Schema
149+
145150
type UserId = UserId of int
146151
147152
module UserId =
@@ -155,15 +160,15 @@ type Account = { Id: UserId; Name: string }
155160
let makeAccount id name = { Id = id; Name = name }
156161
157162
let userIdSchema =
158-
Schema.int
159-
|> Schema.tryMap UserId.create UserId.value
163+
int
164+
|> tryMap UserId.create UserId.value
160165
161166
let accountSchema =
162-
Schema.define<Account>
163-
|> Schema.construct makeAccount
164-
|> Schema.fieldWith "id" _.Id userIdSchema
165-
|> Schema.field "name" _.Name
166-
|> Schema.build
167+
define<Account>
168+
|> construct makeAccount
169+
|> fieldWith "id" _.Id userIdSchema
170+
|> field "name" _.Name
171+
|> build
167172
```
168173

169174
The JSON contract is still:
@@ -177,16 +182,18 @@ The in-memory model is stronger, but you did not need a second DTO type just to
177182
If the wire contract really changes, the schema changes with it in one obvious place:
178183

179184
```fsharp
185+
open CodecMapper.Schema
186+
180187
type PersonV2 = { Id: int; Name: string; Email: string option }
181188
let makePersonV2 id name email = { Id = id; Name = name; Email = email }
182189
183190
let personV2Schema =
184-
Schema.define<PersonV2>
185-
|> Schema.construct makePersonV2
186-
|> Schema.field "id" _.Id
187-
|> Schema.field "name" _.Name
188-
|> Schema.field "email" _.Email
189-
|> Schema.build
191+
define<PersonV2>
192+
|> construct makePersonV2
193+
|> field "id" _.Id
194+
|> field "name" _.Name
195+
|> field "email" _.Email
196+
|> build
190197
```
191198

192199
That does not silently "pick up" the new field just because the record changed. You add it deliberately to the schema, so the contract review point is explicit.
@@ -227,7 +234,8 @@ When benchmark numbers move, profile before changing the runtime. The repo now i
227234
## Docs
228235

229236
- Start with [Getting started](docs/GETTING_STARTED.md).
230-
- Copy from [How to model common contract patterns](docs/HOW_TO_MODEL_COMMON_CONTRACT_PATTERNS.md).
237+
- Use the [contract pattern index](docs/HOW_TO_MODEL_COMMON_CONTRACT_PATTERNS.md) when you need a quick jump page.
238+
- Copy from [How to model a basic record](docs/HOW_TO_MODEL_A_BASIC_RECORD.md), [how to model a nested record](docs/HOW_TO_MODEL_A_NESTED_RECORD.md), [how to model a validated wrapper](docs/HOW_TO_MODEL_A_VALIDATED_WRAPPER.md), or [how to model a versioned contract](docs/HOW_TO_MODEL_A_VERSIONED_CONTRACT.md).
231239
- Use [Configuration contracts guide](docs/CONFIG_CONTRACTS.md) for versioned config shapes.
232240
- Use [How to export JSON Schema](docs/HOW_TO_EXPORT_JSON_SCHEMA.md) and [JSON Schema support reference](docs/JSON_SCHEMA_SUPPORT.md) for schema interchange.
233241
- Use [How to import existing C# contracts](docs/HOW_TO_IMPORT_CSHARP_CONTRACTS.md) for the bridge/facade story.

docs/CONFIG_CONTRACTS.md

Lines changed: 30 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ Use this for flat config-style boundaries only. Collections, raw JSON, and other
6666
When a config field should fall back to a known value only when it is absent, keep that policy explicit in the schema:
6767

6868
```fsharp
69+
open CodecMapper.Schema
70+
6971
type AppConfig =
7072
{
7173
Mode: string
@@ -74,17 +76,17 @@ type AppConfig =
7476
}
7577
7678
let appConfigSchema =
77-
Schema.define<AppConfig>
78-
|> Schema.construct (fun mode retryCount labels ->
79+
define<AppConfig>
80+
|> construct (fun mode retryCount labels ->
7981
{
8082
Mode = mode
8183
RetryCount = retryCount
8284
Labels = labels
8385
})
84-
|> Schema.fieldWith "mode" _.Mode (Schema.string |> Schema.missingAsValue "strict")
85-
|> Schema.fieldWith "retry_count" _.RetryCount (Schema.int |> Schema.missingAsValue 3)
86-
|> Schema.fieldWith "labels" _.Labels (Schema.list Schema.string |> Schema.missingAsValue [])
87-
|> Schema.build
86+
|> fieldWith "mode" _.Mode (string |> missingAsValue "strict")
87+
|> fieldWith "retry_count" _.RetryCount (int |> missingAsValue 3)
88+
|> fieldWith "labels" _.Labels (list string |> missingAsValue [])
89+
|> build
8890
```
8991

9092
That keeps the default local to the contract instead of smuggling it through serializer settings or post-deserialize mutation.
@@ -94,22 +96,24 @@ That keeps the default local to the contract instead of smuggling it through ser
9496
Some config boundaries treat an explicit `null` or an explicit empty collection as "use the contract default" rather than as a distinct payload state. Keep that normalization local to the field too:
9597

9698
```fsharp
99+
open CodecMapper.Schema
100+
97101
type ServiceConfig =
98102
{
99103
Region: string
100104
Labels: string list
101105
}
102106
103107
let serviceConfigSchema =
104-
Schema.define<ServiceConfig>
105-
|> Schema.construct (fun region labels ->
108+
define<ServiceConfig>
109+
|> construct (fun region labels ->
106110
{
107111
Region = region
108112
Labels = labels
109113
})
110-
|> Schema.fieldWith "region" _.Region (Schema.string |> Schema.nullAsValue "global")
111-
|> Schema.fieldWith "labels" _.Labels (Schema.list Schema.string |> Schema.emptyCollectionAsValue [ "general" ])
112-
|> Schema.build
114+
|> fieldWith "region" _.Region (string |> nullAsValue "global")
115+
|> fieldWith "labels" _.Labels (list string |> emptyCollectionAsValue [ "general" ])
116+
|> build
113117
```
114118

115119
That means:
@@ -142,14 +146,14 @@ retry_count: 3
142146
mode: strict
143147
```
144148
145-
Current YAML scope is intentionally narrow:
149+
The YAML projection supports:
146150
147151
- mappings
148152
- sequences
149153
- scalars and `null`
150154
- quoted or plain strings
151155

152-
It does not aim at full YAML feature parity. Anchors, tags, multi-document streams, block scalars, and broader YAML syntax are still out of scope.
156+
Unsupported YAML features include anchors, tags, multi-document streams, block scalars, and broader YAML syntax.
153157

154158
## Recommended Shape
155159

@@ -329,6 +333,8 @@ With `CodecMapper`, the wire contract should be explicit in the schema, not infe
329333
A versioned envelope schema is the right place to make changes visible:
330334

331335
```fsharp
336+
open CodecMapper.Schema
337+
332338
type AppConfigV2 =
333339
{
334340
ServiceUrl: string
@@ -344,29 +350,29 @@ type VersionEnvelope<'T> =
344350
345351
module Schemas =
346352
let appConfigV2 =
347-
Schema.define<AppConfigV2>
348-
|> Schema.construct (fun serviceUrl retryCount mode ->
353+
define<AppConfigV2>
354+
|> construct (fun serviceUrl retryCount mode ->
349355
{
350356
ServiceUrl = serviceUrl
351357
RetryCount = retryCount
352358
Mode = mode
353359
})
354-
|> Schema.field "service_url" _.ServiceUrl
355-
|> Schema.field "retry_count" _.RetryCount
356-
|> Schema.field "mode" _.Mode
357-
|> Schema.build
360+
|> field "service_url" _.ServiceUrl
361+
|> field "retry_count" _.RetryCount
362+
|> field "mode" _.Mode
363+
|> build
358364
```
359365

360366
Then wrap that with a schema for the envelope:
361367

362368
```fsharp
363369
module Schemas =
364370
let versionEnvelope inner =
365-
Schema.define<VersionEnvelope<'T>>
366-
|> Schema.construct (fun version config -> { Version = version; Config = config })
367-
|> Schema.field "version" _.Version
368-
|> Schema.fieldWith "config" _.Config inner
369-
|> Schema.build
371+
define<VersionEnvelope<'T>>
372+
|> construct (fun version config -> { Version = version; Config = config })
373+
|> field "version" _.Version
374+
|> fieldWith "config" _.Config inner
375+
|> build
370376
```
371377

372378
The latest version should be the one you serialize.

0 commit comments

Comments
 (0)