+1
-1
.gitignore
+1
-1
.gitignore
+108
example/README.md
+108
example/README.md
···
···
1
+
# Jetstream Validation Example
2
+
3
+
This example demonstrates using **honk** to validate AT Protocol records from Bluesky's Jetstream firehose in real-time.
4
+
5
+
## What it does
6
+
7
+
1. Connects to Jetstream using **goose** (WebSocket consumer)
8
+
2. Filters for `xyz.statusphere.status` records
9
+
3. Validates each record using **honk**
10
+
4. Displays validation results with emoji status
11
+
12
+
## Running the example
13
+
14
+
```sh
15
+
cd example
16
+
gleam run
17
+
```
18
+
19
+
The example will connect to the live Jetstream firehose and display validation results as records are created:
20
+
21
+
```
22
+
🦢 Honk + Goose: Jetstream Validation Example
23
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
24
+
25
+
Connecting to Jetstream...
26
+
Filtering for: xyz.statusphere.status
27
+
Validating records with honk...
28
+
29
+
✓ VALID | q6gjnaw2blty... | 👍 | 3l4abc123
30
+
✓ VALID | wa7b35aakoll... | 🎉 | 3l4def456
31
+
✗ INVALID | rfov6bpyztcn... | Data: status exceeds maxGraphemes | 3l4ghi789
32
+
✓ UPDATED | eygmaihciaxp... | 😀 | 3l4jkl012
33
+
🗑️ DELETED | ufbl4k27gp6k... | 3l4mno345
34
+
```
35
+
36
+
## How it works
37
+
38
+
### Lexicon Definition
39
+
40
+
The example defines the `xyz.statusphere.status` lexicon:
41
+
42
+
```json
43
+
{
44
+
"lexicon": 1,
45
+
"id": "xyz.statusphere.status",
46
+
"defs": {
47
+
"main": {
48
+
"type": "record",
49
+
"record": {
50
+
"type": "object",
51
+
"required": ["status", "createdAt"],
52
+
"properties": {
53
+
"status": {
54
+
"type": "string",
55
+
"minLength": 1,
56
+
"maxGraphemes": 1,
57
+
"maxLength": 32
58
+
},
59
+
"createdAt": {
60
+
"type": "string",
61
+
"format": "datetime"
62
+
}
63
+
}
64
+
}
65
+
}
66
+
}
67
+
}
68
+
```
69
+
70
+
### Validation Flow
71
+
72
+
1. **goose** receives Jetstream events via WebSocket
73
+
2. Events are parsed into typed Gleam structures
74
+
3. For `create` and `update` operations:
75
+
- Extract the `record` field (contains the status data)
76
+
- Pass to `honk.validate_record()` with the lexicon
77
+
- Display ✓ for valid or ✗ for invalid records
78
+
4. For `delete` operations:
79
+
- Just log the deletion (no record to validate)
80
+
81
+
### Dependencies
82
+
83
+
- **honk**: AT Protocol lexicon validator (local path)
84
+
- **goose**: Jetstream WebSocket consumer library
85
+
- **gleam_json**: JSON encoding/decoding
86
+
- **gleam_stdlib**: Standard library
87
+
88
+
## Code Structure
89
+
90
+
```
91
+
example/
92
+
├── gleam.toml # Dependencies configuration
93
+
├── README.md # This file
94
+
└── src/
95
+
└── example.gleam # Main application
96
+
├── main() # Entry point
97
+
├── handle_event() # Process Jetstream events
98
+
├── handle_create/update() # Validate records
99
+
├── create_statusphere_lexicon()# Define lexicon
100
+
└── format_error/extract_status # Display helpers
101
+
```
102
+
103
+
## Learn More
104
+
105
+
- **honk**: https://hexdocs.pm/honk
106
+
- **goose**: https://hexdocs.pm/goose
107
+
- **Jetstream**: https://docs.bsky.app/docs/advanced-guides/jetstream
108
+
- **AT Protocol**: https://atproto.com/
+12
example/gleam.toml
+12
example/gleam.toml
···
···
1
+
name = "example"
2
+
version = "1.0.0"
3
+
description = "Example using honk to validate xyz.statusphere.status records from Jetstream"
4
+
5
+
[dependencies]
6
+
gleam_stdlib = ">= 0.44.0 and < 2.0.0"
7
+
gleam_json = ">= 3.0.0 and < 4.0.0"
8
+
honk = { path = ".." }
9
+
goose = ">= 2.0.0 and < 3.0.0"
10
+
11
+
[dev-dependencies]
12
+
gleeunit = ">= 1.0.0 and < 2.0.0"
+29
example/manifest.toml
+29
example/manifest.toml
···
···
1
+
# This file was generated by Gleam
2
+
# You typically do not need to edit this file
3
+
4
+
packages = [
5
+
{ name = "exception", version = "2.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "exception", source = "hex", outer_checksum = "329D269D5C2A314F7364BD2711372B6F2C58FA6F39981572E5CA68624D291F8C" },
6
+
{ name = "ezstd", version = "1.2.3", build_tools = ["rebar3"], requirements = [], otp_app = "ezstd", source = "hex", outer_checksum = "DE32E0B41BA36A9ED46DB8215DA74777D2F141BB75F67BFC05DBB4B7C3386DEE" },
7
+
{ name = "filepath", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "B06A9AF0BF10E51401D64B98E4B627F1D2E48C154967DA7AF4D0914780A6D40A" },
8
+
{ name = "gleam_crypto", version = "1.5.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_crypto", source = "hex", outer_checksum = "50774BAFFF1144E7872814C566C5D653D83A3EBF23ACC3156B757A1B6819086E" },
9
+
{ name = "gleam_erlang", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "1124AD3AA21143E5AF0FC5CF3D9529F6DB8CA03E43A55711B60B6B7B3874375C" },
10
+
{ name = "gleam_http", version = "4.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "82EA6A717C842456188C190AFB372665EA56CE13D8559BF3B1DD9E40F619EE0C" },
11
+
{ name = "gleam_json", version = "3.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "44FDAA8847BE8FC48CA7A1C089706BD54BADCC4C45B237A992EDDF9F2CDB2836" },
12
+
{ name = "gleam_otp", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "BA6A294E295E428EC1562DC1C11EA7530DCB981E8359134BEABC8493B7B2258E" },
13
+
{ name = "gleam_regexp", version = "1.1.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_regexp", source = "hex", outer_checksum = "9C215C6CA84A5B35BB934A9B61A9A306EC743153BE2B0425A0D032E477B062A9" },
14
+
{ name = "gleam_stdlib", version = "0.65.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "7C69C71D8C493AE11A5184828A77110EB05A7786EBF8B25B36A72F879C3EE107" },
15
+
{ name = "gleam_time", version = "1.5.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_time", source = "hex", outer_checksum = "D560E672C7279C89908981E068DF07FD16D0C859DCA266F908B18F04DF0EB8E6" },
16
+
{ name = "gleeunit", version = "1.9.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "DA9553CE58B67924B3C631F96FE3370C49EB6D6DC6B384EC4862CC4AAA718F3C" },
17
+
{ name = "goose", version = "2.0.0", build_tools = ["gleam"], requirements = ["exception", "ezstd", "gleam_crypto", "gleam_erlang", "gleam_http", "gleam_json", "gleam_otp", "gleam_stdlib", "gramps", "logging", "simplifile"], otp_app = "goose", source = "hex", outer_checksum = "E991B275766D28693B8179EF77ADCCD210D58C1D3E3A1B4539C228D6CE58845B" },
18
+
{ name = "gramps", version = "6.0.0", build_tools = ["gleam"], requirements = ["gleam_crypto", "gleam_erlang", "gleam_http", "gleam_stdlib"], otp_app = "gramps", source = "hex", outer_checksum = "8B7195978FBFD30B43DF791A8A272041B81E45D245314D7A41FC57237AA882A0" },
19
+
{ name = "honk", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_regexp", "gleam_stdlib", "gleam_time"], source = "local", path = ".." },
20
+
{ name = "logging", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "logging", source = "hex", outer_checksum = "1098FBF10B54B44C2C7FDF0B01C1253CAFACDACABEFB4B0D027803246753E06D" },
21
+
{ name = "simplifile", version = "2.3.1", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "957E0E5B75927659F1D2A1B7B75D7B9BA96FAA8D0C53EA71C4AD9CD0C6B848F6" },
22
+
]
23
+
24
+
[requirements]
25
+
gleam_json = { version = ">= 3.0.0 and < 4.0.0" }
26
+
gleam_stdlib = { version = ">= 0.44.0 and < 2.0.0" }
27
+
gleeunit = { version = ">= 1.0.0 and < 2.0.0" }
28
+
goose = { version = ">= 2.0.0 and < 3.0.0" }
29
+
honk = { path = ".." }
+250
example/src/example.gleam
+250
example/src/example.gleam
···
···
1
+
// Example: Validating xyz.statusphere.status records from Jetstream using honk
2
+
//
3
+
// This example connects to Bluesky's Jetstream firehose, filters for
4
+
// xyz.statusphere.status records, and validates them in real-time using honk.
5
+
6
+
import gleam/dynamic/decode
7
+
import gleam/io
8
+
import gleam/json
9
+
import gleam/option
10
+
import gleam/string
11
+
import goose
12
+
import honk
13
+
import honk/errors.{DataValidation, InvalidSchema, LexiconNotFound}
14
+
15
+
pub fn main() {
16
+
io.println("🦢 Honk + Goose: Jetstream Validation Example")
17
+
io.println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
18
+
io.println("")
19
+
io.println("Connecting to Jetstream...")
20
+
io.println("Filtering for: xyz.statusphere.status")
21
+
io.println("Validating records with honk...")
22
+
io.println("")
23
+
24
+
// Define the xyz.statusphere.status lexicon
25
+
let lexicon = create_statusphere_lexicon()
26
+
27
+
// Configure goose to connect to Jetstream
28
+
let config =
29
+
goose.JetstreamConfig(
30
+
endpoint: "wss://jetstream2.us-west.bsky.network/subscribe",
31
+
wanted_collections: ["xyz.statusphere.status"],
32
+
wanted_dids: [],
33
+
cursor: option.None,
34
+
max_message_size_bytes: option.None,
35
+
compress: True,
36
+
require_hello: False,
37
+
)
38
+
39
+
// Start consuming events (this blocks forever)
40
+
goose.start_consumer(config, handle_event(_, lexicon))
41
+
}
42
+
43
+
/// Handles each Jetstream event
44
+
fn handle_event(json_event: String, lexicon: json.Json) -> Nil {
45
+
let event = goose.parse_event(json_event)
46
+
47
+
case event {
48
+
// Handle commit events (create/update/delete)
49
+
goose.CommitEvent(did, time_us, commit) -> {
50
+
case commit.operation {
51
+
"create" -> handle_create(did, time_us, commit, lexicon)
52
+
"update" -> handle_update(did, time_us, commit, lexicon)
53
+
"delete" -> handle_delete(did, time_us, commit)
54
+
_ -> Nil
55
+
}
56
+
}
57
+
58
+
// Ignore identity and account events for this example
59
+
goose.IdentityEvent(_, _, _) -> Nil
60
+
goose.AccountEvent(_, _, _) -> Nil
61
+
goose.UnknownEvent(raw) -> {
62
+
io.println("⚠️ Unknown event: " <> raw)
63
+
}
64
+
}
65
+
}
66
+
67
+
/// Handles create operations - validates the new record
68
+
fn handle_create(
69
+
did: String,
70
+
_time_us: Int,
71
+
commit: goose.CommitData,
72
+
lexicon: json.Json,
73
+
) -> Nil {
74
+
case commit.record {
75
+
option.Some(record_dynamic) -> {
76
+
// Convert Dynamic to JSON for honk validation
77
+
let record_json = dynamic_to_json(record_dynamic)
78
+
79
+
// Validate the record using honk
80
+
case
81
+
honk.validate_record([lexicon], "xyz.statusphere.status", record_json)
82
+
{
83
+
Ok(_) -> {
84
+
// Extract status emoji for display
85
+
let status_emoji = extract_status(record_dynamic)
86
+
io.println(
87
+
"✓ VALID | "
88
+
<> truncate_did(did)
89
+
<> " | "
90
+
<> status_emoji
91
+
<> " | "
92
+
<> commit.rkey,
93
+
)
94
+
}
95
+
Error(err) -> {
96
+
io.println(
97
+
"✗ INVALID | "
98
+
<> truncate_did(did)
99
+
<> " | "
100
+
<> format_error(err)
101
+
<> " | "
102
+
<> commit.rkey,
103
+
)
104
+
}
105
+
}
106
+
}
107
+
option.None -> {
108
+
io.println("⚠️ CREATE event without record data")
109
+
}
110
+
}
111
+
}
112
+
113
+
/// Handles update operations - validates the updated record
114
+
fn handle_update(
115
+
did: String,
116
+
_time_us: Int,
117
+
commit: goose.CommitData,
118
+
lexicon: json.Json,
119
+
) -> Nil {
120
+
case commit.record {
121
+
option.Some(record_dynamic) -> {
122
+
let record_json = dynamic_to_json(record_dynamic)
123
+
124
+
case
125
+
honk.validate_record([lexicon], "xyz.statusphere.status", record_json)
126
+
{
127
+
Ok(_) -> {
128
+
let status_emoji = extract_status(record_dynamic)
129
+
io.println(
130
+
"✓ UPDATED | "
131
+
<> truncate_did(did)
132
+
<> " | "
133
+
<> status_emoji
134
+
<> " | "
135
+
<> commit.rkey,
136
+
)
137
+
}
138
+
Error(err) -> {
139
+
io.println(
140
+
"✗ INVALID | "
141
+
<> truncate_did(did)
142
+
<> " | "
143
+
<> format_error(err)
144
+
<> " | "
145
+
<> commit.rkey,
146
+
)
147
+
}
148
+
}
149
+
}
150
+
option.None -> {
151
+
io.println("⚠️ UPDATE event without record data")
152
+
}
153
+
}
154
+
}
155
+
156
+
/// Handles delete operations - no validation needed
157
+
fn handle_delete(did: String, _time_us: Int, commit: goose.CommitData) -> Nil {
158
+
io.println("🗑️ DELETED | " <> truncate_did(did) <> " | " <> commit.rkey)
159
+
}
160
+
161
+
/// Creates the xyz.statusphere.status lexicon definition
162
+
fn create_statusphere_lexicon() -> json.Json {
163
+
json.object([
164
+
#("lexicon", json.int(1)),
165
+
#("id", json.string("xyz.statusphere.status")),
166
+
#(
167
+
"defs",
168
+
json.object([
169
+
#(
170
+
"main",
171
+
json.object([
172
+
#("type", json.string("record")),
173
+
#("key", json.string("tid")),
174
+
#(
175
+
"record",
176
+
json.object([
177
+
#("type", json.string("object")),
178
+
#(
179
+
"required",
180
+
json.preprocessed_array([
181
+
json.string("status"),
182
+
json.string("createdAt"),
183
+
]),
184
+
),
185
+
#(
186
+
"properties",
187
+
json.object([
188
+
#(
189
+
"status",
190
+
json.object([
191
+
#("type", json.string("string")),
192
+
#("minLength", json.int(1)),
193
+
#("maxGraphemes", json.int(1)),
194
+
#("maxLength", json.int(32)),
195
+
]),
196
+
),
197
+
#(
198
+
"createdAt",
199
+
json.object([
200
+
#("type", json.string("string")),
201
+
#("format", json.string("datetime")),
202
+
]),
203
+
),
204
+
]),
205
+
),
206
+
]),
207
+
),
208
+
]),
209
+
),
210
+
]),
211
+
),
212
+
])
213
+
}
214
+
215
+
/// Converts Dynamic to Json (they're the same underlying type)
216
+
@external(erlang, "gleam@dynamic", "unsafe_coerce")
217
+
fn dynamic_to_json(value: decode.Dynamic) -> json.Json
218
+
219
+
/// Extracts the status emoji from a record for display
220
+
fn extract_status(record: decode.Dynamic) -> String {
221
+
let decoder = {
222
+
use status <- decode.field("status", decode.string)
223
+
decode.success(status)
224
+
}
225
+
case decode.run(record, decoder) {
226
+
Ok(status) -> status
227
+
Error(_) -> "�"
228
+
}
229
+
}
230
+
231
+
/// Formats a validation error for display
232
+
fn format_error(err: honk.ValidationError) -> String {
233
+
case err {
234
+
InvalidSchema(msg) -> "Schema: " <> msg
235
+
DataValidation(msg) -> "Data: " <> msg
236
+
LexiconNotFound(id) -> "Not found: " <> id
237
+
}
238
+
}
239
+
240
+
/// Truncates a DID for cleaner display
241
+
fn truncate_did(did: String) -> String {
242
+
case string.split(did, ":") {
243
+
[_, _, suffix] ->
244
+
case string.length(suffix) > 12 {
245
+
True -> string.slice(suffix, 0, 12) <> "..."
246
+
False -> suffix
247
+
}
248
+
_ -> did
249
+
}
250
+
}
+13
example/test/example_test.gleam
+13
example/test/example_test.gleam
src/errors.gleam
src/honk/errors.gleam
src/errors.gleam
src/honk/errors.gleam
+27
-59
src/honk.gleam
+27
-59
src/honk.gleam
···
1
// Main public API for the ATProtocol lexicon validator
2
3
-
import errors.{type ValidationError}
4
import gleam/dict.{type Dict}
5
import gleam/json.{type Json}
6
import gleam/option.{None, Some}
7
import gleam/result
8
import honk/internal/json_helpers
9
-
import types
10
-
import validation/context
11
-
import validation/formats
12
13
// Import validators
14
-
import validation/field as validation_field
15
-
import validation/field/reference as validation_field_reference
16
-
import validation/field/union as validation_field_union
17
-
import validation/meta/token as validation_meta_token
18
-
import validation/meta/unknown as validation_meta_unknown
19
-
import validation/primary/params as validation_primary_params
20
-
import validation/primary/procedure as validation_primary_procedure
21
-
import validation/primary/query as validation_primary_query
22
-
import validation/primary/record as validation_primary_record
23
-
import validation/primary/subscription as validation_primary_subscription
24
-
import validation/primitive/blob as validation_primitive_blob
25
-
import validation/primitive/boolean as validation_primitive_boolean
26
-
import validation/primitive/bytes as validation_primitive_bytes
27
-
import validation/primitive/cid_link as validation_primitive_cid_link
28
-
import validation/primitive/integer as validation_primitive_integer
29
-
import validation/primitive/null as validation_primitive_null
30
-
import validation/primitive/string as validation_primitive_string
31
32
-
// Re-export core types
33
-
pub type LexiconDoc =
34
-
types.LexiconDoc
35
-
36
-
pub type StringFormat {
37
-
DateTime
38
-
Uri
39
-
AtUri
40
-
Did
41
-
Handle
42
-
AtIdentifier
43
-
Nsid
44
-
Cid
45
-
Language
46
-
Tid
47
-
RecordKey
48
-
}
49
-
50
-
pub type ValidationContext =
51
-
context.ValidationContext
52
53
/// Main validation function for lexicon documents
54
/// Returns Ok(Nil) if all lexicons are valid
···
165
/// Validates a string value against a specific format
166
pub fn validate_string_format(
167
value: String,
168
-
format: StringFormat,
169
) -> Result(Nil, String) {
170
-
// Convert our StringFormat to types.StringFormat
171
-
let types_format = case format {
172
-
DateTime -> types.DateTime
173
-
Uri -> types.Uri
174
-
AtUri -> types.AtUri
175
-
Did -> types.Did
176
-
Handle -> types.Handle
177
-
AtIdentifier -> types.AtIdentifier
178
-
Nsid -> types.Nsid
179
-
Cid -> types.Cid
180
-
Language -> types.Language
181
-
Tid -> types.Tid
182
-
RecordKey -> types.RecordKey
183
-
}
184
-
185
-
case formats.validate_format(value, types_format) {
186
True -> Ok(Nil)
187
False -> {
188
-
let format_name = types.format_to_string(types_format)
189
Error("Value does not match format: " <> format_name)
190
}
191
}
···
1
// Main public API for the ATProtocol lexicon validator
2
3
+
import honk/errors as errors
4
import gleam/dict.{type Dict}
5
import gleam/json.{type Json}
6
import gleam/option.{None, Some}
7
import gleam/result
8
import honk/internal/json_helpers
9
+
import honk/types as types
10
+
import honk/validation/context
11
+
import honk/validation/formats
12
13
// Import validators
14
+
import honk/validation/field as validation_field
15
+
import honk/validation/field/reference as validation_field_reference
16
+
import honk/validation/field/union as validation_field_union
17
+
import honk/validation/meta/token as validation_meta_token
18
+
import honk/validation/meta/unknown as validation_meta_unknown
19
+
import honk/validation/primary/params as validation_primary_params
20
+
import honk/validation/primary/procedure as validation_primary_procedure
21
+
import honk/validation/primary/query as validation_primary_query
22
+
import honk/validation/primary/record as validation_primary_record
23
+
import honk/validation/primary/subscription as validation_primary_subscription
24
+
import honk/validation/primitive/blob as validation_primitive_blob
25
+
import honk/validation/primitive/boolean as validation_primitive_boolean
26
+
import honk/validation/primitive/bytes as validation_primitive_bytes
27
+
import honk/validation/primitive/cid_link as validation_primitive_cid_link
28
+
import honk/validation/primitive/integer as validation_primitive_integer
29
+
import honk/validation/primitive/null as validation_primitive_null
30
+
import honk/validation/primitive/string as validation_primitive_string
31
32
+
// Re-export error type for public API error handling
33
+
pub type ValidationError =
34
+
errors.ValidationError
35
36
/// Main validation function for lexicon documents
37
/// Returns Ok(Nil) if all lexicons are valid
···
148
/// Validates a string value against a specific format
149
pub fn validate_string_format(
150
value: String,
151
+
format: types.StringFormat,
152
) -> Result(Nil, String) {
153
+
case formats.validate_format(value, format) {
154
True -> Ok(Nil)
155
False -> {
156
+
let format_name = types.format_to_string(format)
157
Error("Value does not match format: " <> format_name)
158
}
159
}
+8
-8
src/honk/internal/constraints.gleam
+8
-8
src/honk/internal/constraints.gleam
···
1
// Reusable constraint validation functions
2
3
-
import errors.{type ValidationError}
4
import gleam/int
5
import gleam/list
6
import gleam/option.{type Option, Some}
···
14
min_length: Option(Int),
15
max_length: Option(Int),
16
type_name: String,
17
-
) -> Result(Nil, ValidationError) {
18
// Check minimum length
19
case min_length {
20
Some(min) if actual_length < min ->
···
53
min_length: Option(Int),
54
max_length: Option(Int),
55
type_name: String,
56
-
) -> Result(Nil, ValidationError) {
57
case min_length, max_length {
58
Some(min), Some(max) if min > max ->
59
Error(errors.invalid_schema(
···
76
value: Int,
77
minimum: Option(Int),
78
maximum: Option(Int),
79
-
) -> Result(Nil, ValidationError) {
80
// Check minimum
81
case minimum {
82
Some(min) if value < min ->
···
110
def_name: String,
111
minimum: Option(Int),
112
maximum: Option(Int),
113
-
) -> Result(Nil, ValidationError) {
114
case minimum, maximum {
115
Some(min), Some(max) if min > max ->
116
Error(errors.invalid_schema(
···
135
type_name: String,
136
to_string: fn(a) -> String,
137
equal: fn(a, a) -> Bool,
138
-
) -> Result(Nil, ValidationError) {
139
let found = list.any(enum_values, fn(enum_val) { equal(value, enum_val) })
140
141
case found {
···
158
has_const: Bool,
159
has_default: Bool,
160
type_name: String,
161
-
) -> Result(Nil, ValidationError) {
162
case has_const, has_default {
163
True, True ->
164
Error(errors.invalid_schema(
···
177
actual_fields: List(String),
178
allowed_fields: List(String),
179
type_name: String,
180
-
) -> Result(Nil, ValidationError) {
181
let unknown_fields =
182
list.filter(actual_fields, fn(field) {
183
!list.contains(allowed_fields, field)
···
1
// Reusable constraint validation functions
2
3
+
import honk/errors as errors
4
import gleam/int
5
import gleam/list
6
import gleam/option.{type Option, Some}
···
14
min_length: Option(Int),
15
max_length: Option(Int),
16
type_name: String,
17
+
) -> Result(Nil, errors.ValidationError) {
18
// Check minimum length
19
case min_length {
20
Some(min) if actual_length < min ->
···
53
min_length: Option(Int),
54
max_length: Option(Int),
55
type_name: String,
56
+
) -> Result(Nil, errors.ValidationError) {
57
case min_length, max_length {
58
Some(min), Some(max) if min > max ->
59
Error(errors.invalid_schema(
···
76
value: Int,
77
minimum: Option(Int),
78
maximum: Option(Int),
79
+
) -> Result(Nil, errors.ValidationError) {
80
// Check minimum
81
case minimum {
82
Some(min) if value < min ->
···
110
def_name: String,
111
minimum: Option(Int),
112
maximum: Option(Int),
113
+
) -> Result(Nil, errors.ValidationError) {
114
case minimum, maximum {
115
Some(min), Some(max) if min > max ->
116
Error(errors.invalid_schema(
···
135
type_name: String,
136
to_string: fn(a) -> String,
137
equal: fn(a, a) -> Bool,
138
+
) -> Result(Nil, errors.ValidationError) {
139
let found = list.any(enum_values, fn(enum_val) { equal(value, enum_val) })
140
141
case found {
···
158
has_const: Bool,
159
has_default: Bool,
160
type_name: String,
161
+
) -> Result(Nil, errors.ValidationError) {
162
case has_const, has_default {
163
True, True ->
164
Error(errors.invalid_schema(
···
177
actual_fields: List(String),
178
allowed_fields: List(String),
179
type_name: String,
180
+
) -> Result(Nil, errors.ValidationError) {
181
let unknown_fields =
182
list.filter(actual_fields, fn(field) {
183
!list.contains(allowed_fields, field)
+7
-7
src/honk/internal/json_helpers.gleam
+7
-7
src/honk/internal/json_helpers.gleam
···
1
// JSON helper utilities for extracting and validating fields
2
3
-
import errors.{type ValidationError}
4
import gleam/dict.{type Dict}
5
import gleam/dynamic.{type Dynamic}
6
import gleam/dynamic/decode
···
153
case get_string(json_value, field_name) {
154
Some(s) -> Ok(s)
155
None ->
156
-
Error(errors.invalid_schema(
157
def_name <> ": '" <> field_name <> "' must be a string",
158
))
159
}
···
168
case get_int(json_value, field_name) {
169
Some(i) -> Ok(i)
170
None ->
171
-
Error(errors.invalid_schema(
172
def_name <> ": '" <> field_name <> "' must be an integer",
173
))
174
}
···
183
case get_array(json_value, field_name) {
184
Some(arr) -> Ok(arr)
185
None ->
186
-
Error(errors.invalid_schema(
187
def_name <> ": '" <> field_name <> "' must be an array",
188
))
189
}
···
236
case decode.run(dyn, decode.dict(decode.string, decode.dynamic)) {
237
Ok(dict_val) -> Ok(dict_val)
238
Error(_) ->
239
-
Error(errors.data_validation("Failed to convert JSON to dictionary"))
240
}
241
-
Error(_) -> Error(errors.data_validation("Failed to parse JSON as dynamic"))
242
}
243
}
244
···
293
}
294
}
295
Error(_) ->
296
-
Error(errors.data_validation(
297
"Failed to convert dynamic to Json",
298
))
299
}
···
1
// JSON helper utilities for extracting and validating fields
2
3
+
import honk/errors.{type ValidationError, data_validation, invalid_schema}
4
import gleam/dict.{type Dict}
5
import gleam/dynamic.{type Dynamic}
6
import gleam/dynamic/decode
···
153
case get_string(json_value, field_name) {
154
Some(s) -> Ok(s)
155
None ->
156
+
Error(invalid_schema(
157
def_name <> ": '" <> field_name <> "' must be a string",
158
))
159
}
···
168
case get_int(json_value, field_name) {
169
Some(i) -> Ok(i)
170
None ->
171
+
Error(invalid_schema(
172
def_name <> ": '" <> field_name <> "' must be an integer",
173
))
174
}
···
183
case get_array(json_value, field_name) {
184
Some(arr) -> Ok(arr)
185
None ->
186
+
Error(invalid_schema(
187
def_name <> ": '" <> field_name <> "' must be an array",
188
))
189
}
···
236
case decode.run(dyn, decode.dict(decode.string, decode.dynamic)) {
237
Ok(dict_val) -> Ok(dict_val)
238
Error(_) ->
239
+
Error(data_validation("Failed to convert JSON to dictionary"))
240
}
241
+
Error(_) -> Error(data_validation("Failed to parse JSON as dynamic"))
242
}
243
}
244
···
293
}
294
}
295
Error(_) ->
296
+
Error(data_validation(
297
"Failed to convert dynamic to Json",
298
))
299
}
+7
-7
src/honk/internal/resolution.gleam
+7
-7
src/honk/internal/resolution.gleam
···
1
// Reference resolution utilities
2
3
-
import errors.{type ValidationError}
4
import gleam/dict.{type Dict}
5
import gleam/json.{type Json}
6
import gleam/list
···
9
import gleam/set.{type Set}
10
import gleam/string
11
import honk/internal/json_helpers
12
-
import validation/context.{type ValidationContext}
13
14
/// Resolves a reference string to its target definition
15
pub fn resolve_reference(
16
reference: String,
17
ctx: ValidationContext,
18
current_lexicon_id: String,
19
-
) -> Result(Option(Json), ValidationError) {
20
// Update context with current lexicon
21
let ctx = context.with_current_lexicon(ctx, current_lexicon_id)
22
···
55
ctx: ValidationContext,
56
current_lexicon_id: String,
57
def_path: String,
58
-
) -> Result(Nil, ValidationError) {
59
// Check for circular reference
60
case context.has_reference(ctx, reference) {
61
True ->
···
119
pub fn validate_lexicon_references(
120
lexicon_id: String,
121
ctx: ValidationContext,
122
-
) -> Result(Nil, ValidationError) {
123
case context.get_lexicon(ctx, lexicon_id) {
124
Some(lexicon) -> {
125
// Collect all references from the lexicon
···
143
/// Validates completeness of all lexicons
144
pub fn validate_lexicon_completeness(
145
ctx: ValidationContext,
146
-
) -> Result(Nil, ValidationError) {
147
// Get all lexicon IDs
148
let lexicon_ids = dict.keys(ctx.lexicons)
149
···
156
/// Detects circular dependencies in lexicon references
157
pub fn detect_circular_dependencies(
158
ctx: ValidationContext,
159
-
) -> Result(Nil, ValidationError) {
160
// Build dependency graph
161
let graph = build_dependency_graph(ctx)
162
···
1
// Reference resolution utilities
2
3
+
import honk/errors as errors
4
import gleam/dict.{type Dict}
5
import gleam/json.{type Json}
6
import gleam/list
···
9
import gleam/set.{type Set}
10
import gleam/string
11
import honk/internal/json_helpers
12
+
import honk/validation/context.{type ValidationContext}
13
14
/// Resolves a reference string to its target definition
15
pub fn resolve_reference(
16
reference: String,
17
ctx: ValidationContext,
18
current_lexicon_id: String,
19
+
) -> Result(Option(Json), errors.ValidationError) {
20
// Update context with current lexicon
21
let ctx = context.with_current_lexicon(ctx, current_lexicon_id)
22
···
55
ctx: ValidationContext,
56
current_lexicon_id: String,
57
def_path: String,
58
+
) -> Result(Nil, errors.ValidationError) {
59
// Check for circular reference
60
case context.has_reference(ctx, reference) {
61
True ->
···
119
pub fn validate_lexicon_references(
120
lexicon_id: String,
121
ctx: ValidationContext,
122
+
) -> Result(Nil, errors.ValidationError) {
123
case context.get_lexicon(ctx, lexicon_id) {
124
Some(lexicon) -> {
125
// Collect all references from the lexicon
···
143
/// Validates completeness of all lexicons
144
pub fn validate_lexicon_completeness(
145
ctx: ValidationContext,
146
+
) -> Result(Nil, errors.ValidationError) {
147
// Get all lexicon IDs
148
let lexicon_ids = dict.keys(ctx.lexicons)
149
···
156
/// Detects circular dependencies in lexicon references
157
pub fn detect_circular_dependencies(
158
ctx: ValidationContext,
159
+
) -> Result(Nil, errors.ValidationError) {
160
// Build dependency graph
161
let graph = build_dependency_graph(ctx)
162
src/types.gleam
src/honk/types.gleam
src/types.gleam
src/honk/types.gleam
+22
-14
src/validation/context.gleam
src/honk/validation/context.gleam
+22
-14
src/validation/context.gleam
src/honk/validation/context.gleam
···
1
// Validation context and builder
2
3
-
import errors.{type ValidationError}
4
import gleam/dict.{type Dict}
5
import gleam/json.{type Json}
6
import gleam/list
···
9
import gleam/set.{type Set}
10
import gleam/string
11
import honk/internal/json_helpers
12
-
import types.{type LexiconDoc, LexiconDoc}
13
-
import validation/formats
14
15
/// Validation context that tracks state during validation
16
pub type ValidationContext {
17
ValidationContext(
18
// Map of lexicon ID to parsed lexicon document
19
-
lexicons: Dict(String, LexiconDoc),
20
// Current path in data structure (for error messages)
21
path: String,
22
// Current lexicon ID (for resolving local references)
···
25
reference_stack: Set(String),
26
// Recursive validator function for dispatching to type-specific validators
27
// Parameters: data (Json), schema (Json), ctx (ValidationContext)
28
-
validator: fn(Json, Json, ValidationContext) -> Result(Nil, ValidationError),
29
)
30
}
31
32
/// Builder for constructing ValidationContext
33
pub type ValidationContextBuilder {
34
ValidationContextBuilder(
35
-
lexicons: Dict(String, LexiconDoc),
36
// Parameters: data (Json), schema (Json), ctx (ValidationContext)
37
validator: Option(
38
-
fn(Json, Json, ValidationContext) -> Result(Nil, ValidationError),
39
),
40
)
41
}
···
79
pub fn with_lexicons(
80
builder: ValidationContextBuilder,
81
lexicons: List(Json),
82
-
) -> Result(ValidationContextBuilder, ValidationError) {
83
// Parse each lexicon and add to the dictionary
84
list.try_fold(lexicons, builder, fn(b, lex_json) {
85
// Extract id and defs from the lexicon JSON
···
98
/// Parameters: data (Json), schema (Json), ctx (ValidationContext)
99
pub fn with_validator(
100
builder: ValidationContextBuilder,
101
-
validator: fn(Json, Json, ValidationContext) -> Result(Nil, ValidationError),
102
) -> ValidationContextBuilder {
103
ValidationContextBuilder(..builder, validator: Some(validator))
104
}
···
119
/// ```
120
pub fn build(
121
builder: ValidationContextBuilder,
122
-
) -> Result(ValidationContext, ValidationError) {
123
// Create a default no-op validator if none is set
124
let validator = case builder.validator {
125
Some(v) -> v
···
148
/// None -> // Lexicon not found
149
/// }
150
/// ```
151
-
pub fn get_lexicon(ctx: ValidationContext, id: String) -> Option(LexiconDoc) {
152
case dict.get(ctx.lexicons, id) {
153
Ok(lex) -> Some(lex)
154
Error(_) -> None
···
269
pub fn parse_reference(
270
ctx: ValidationContext,
271
reference: String,
272
-
) -> Result(#(String, String), ValidationError) {
273
case string.split(reference, "#") {
274
// Local reference: #def
275
["", def] ->
···
292
}
293
294
/// Helper to parse a lexicon JSON into LexiconDoc
295
-
fn parse_lexicon(lex_json: Json) -> Result(LexiconDoc, ValidationError) {
296
// Extract "id" field (required NSID)
297
let id_result = case json_helpers.get_string(lex_json, "id") {
298
Some(id) -> Ok(id)
···
328
329
use defs <- result.try(defs_result)
330
331
-
Ok(LexiconDoc(id: id, defs: defs))
332
}
···
1
// Validation context and builder
2
3
+
import honk/errors as errors
4
import gleam/dict.{type Dict}
5
import gleam/json.{type Json}
6
import gleam/list
···
9
import gleam/set.{type Set}
10
import gleam/string
11
import honk/internal/json_helpers
12
+
import honk/types as types
13
+
import honk/validation/formats
14
15
/// Validation context that tracks state during validation
16
pub type ValidationContext {
17
ValidationContext(
18
// Map of lexicon ID to parsed lexicon document
19
+
lexicons: Dict(String, types.LexiconDoc),
20
// Current path in data structure (for error messages)
21
path: String,
22
// Current lexicon ID (for resolving local references)
···
25
reference_stack: Set(String),
26
// Recursive validator function for dispatching to type-specific validators
27
// Parameters: data (Json), schema (Json), ctx (ValidationContext)
28
+
validator: fn(Json, Json, ValidationContext) ->
29
+
Result(Nil, errors.ValidationError),
30
)
31
}
32
33
/// Builder for constructing ValidationContext
34
pub type ValidationContextBuilder {
35
ValidationContextBuilder(
36
+
lexicons: Dict(String, types.LexiconDoc),
37
// Parameters: data (Json), schema (Json), ctx (ValidationContext)
38
validator: Option(
39
+
fn(Json, Json, ValidationContext) ->
40
+
Result(Nil, errors.ValidationError),
41
),
42
)
43
}
···
81
pub fn with_lexicons(
82
builder: ValidationContextBuilder,
83
lexicons: List(Json),
84
+
) -> Result(ValidationContextBuilder, errors.ValidationError) {
85
// Parse each lexicon and add to the dictionary
86
list.try_fold(lexicons, builder, fn(b, lex_json) {
87
// Extract id and defs from the lexicon JSON
···
100
/// Parameters: data (Json), schema (Json), ctx (ValidationContext)
101
pub fn with_validator(
102
builder: ValidationContextBuilder,
103
+
validator: fn(Json, Json, ValidationContext) ->
104
+
Result(Nil, errors.ValidationError),
105
) -> ValidationContextBuilder {
106
ValidationContextBuilder(..builder, validator: Some(validator))
107
}
···
122
/// ```
123
pub fn build(
124
builder: ValidationContextBuilder,
125
+
) -> Result(ValidationContext, errors.ValidationError) {
126
// Create a default no-op validator if none is set
127
let validator = case builder.validator {
128
Some(v) -> v
···
151
/// None -> // Lexicon not found
152
/// }
153
/// ```
154
+
pub fn get_lexicon(
155
+
ctx: ValidationContext,
156
+
id: String,
157
+
) -> Option(types.LexiconDoc) {
158
case dict.get(ctx.lexicons, id) {
159
Ok(lex) -> Some(lex)
160
Error(_) -> None
···
275
pub fn parse_reference(
276
ctx: ValidationContext,
277
reference: String,
278
+
) -> Result(#(String, String), errors.ValidationError) {
279
case string.split(reference, "#") {
280
// Local reference: #def
281
["", def] ->
···
298
}
299
300
/// Helper to parse a lexicon JSON into LexiconDoc
301
+
fn parse_lexicon(
302
+
lex_json: Json,
303
+
) -> Result(types.LexiconDoc, errors.ValidationError) {
304
// Extract "id" field (required NSID)
305
let id_result = case json_helpers.get_string(lex_json, "id") {
306
Some(id) -> Ok(id)
···
336
337
use defs <- result.try(defs_result)
338
339
+
Ok(types.LexiconDoc(id: id, defs: defs))
340
}
+28
-28
src/validation/field.gleam
src/honk/validation/field.gleam
+28
-28
src/validation/field.gleam
src/honk/validation/field.gleam
···
1
// Field type validators (object and array)
2
3
-
import errors.{type ValidationError}
4
import gleam/dict
5
import gleam/dynamic.{type Dynamic}
6
import gleam/dynamic/decode
···
11
import gleam/result
12
import honk/internal/constraints
13
import honk/internal/json_helpers
14
-
import validation/context.{type ValidationContext}
15
16
// Import primitive validators
17
-
import validation/primitive/blob
18
-
import validation/primitive/boolean
19
-
import validation/primitive/bytes
20
-
import validation/primitive/cid_link
21
-
import validation/primitive/integer
22
-
import validation/primitive/null
23
-
import validation/primitive/string
24
25
// Import other field validators
26
-
import validation/field/reference
27
-
import validation/field/union
28
29
// Import meta validators
30
-
import validation/meta/token
31
-
import validation/meta/unknown
32
33
// ============================================================================
34
// SHARED TYPE DISPATCHER
···
39
fn dispatch_schema_validation(
40
schema: Json,
41
ctx: ValidationContext,
42
-
) -> Result(Nil, ValidationError) {
43
case json_helpers.get_string(schema, "type") {
44
Some("string") -> string.validate_schema(schema, ctx)
45
Some("integer") -> integer.validate_schema(schema, ctx)
···
91
data: Json,
92
schema: Json,
93
ctx: ValidationContext,
94
-
) -> Result(Nil, ValidationError) {
95
case json_helpers.get_string(schema, "type") {
96
Some("string") -> string.validate_data(data, schema, ctx)
97
Some("integer") -> integer.validate_data(data, schema, ctx)
···
133
pub fn validate_object_schema(
134
schema: Json,
135
ctx: ValidationContext,
136
-
) -> Result(Nil, ValidationError) {
137
let def_name = context.path(ctx)
138
139
// Validate allowed fields
···
192
data: Json,
193
schema: Json,
194
ctx: ValidationContext,
195
-
) -> Result(Nil, ValidationError) {
196
let def_name = context.path(ctx)
197
198
// Check data is an object
···
235
fn validate_required_fields(
236
def_name: String,
237
required: List(Dynamic),
238
-
) -> Result(Nil, ValidationError) {
239
// Convert dynamics to strings
240
let field_names =
241
list.filter_map(required, fn(item) { decode.run(item, decode.string) })
···
255
fn validate_nullable_fields(
256
def_name: String,
257
nullable: List(Dynamic),
258
-
) -> Result(Nil, ValidationError) {
259
// Convert dynamics to strings
260
let field_names =
261
list.filter_map(nullable, fn(item) { decode.run(item, decode.string) })
···
276
def_name: String,
277
required: List(Dynamic),
278
data: Json,
279
-
) -> Result(Nil, ValidationError) {
280
// Convert dynamics to strings
281
let field_names =
282
list.filter_map(required, fn(item) { decode.run(item, decode.string) })
···
299
fn validate_property_schemas(
300
properties: Json,
301
ctx: ValidationContext,
302
-
) -> Result(Nil, ValidationError) {
303
// Convert JSON object to dict and validate each property
304
case json_helpers.json_to_dict(properties) {
305
Ok(props_dict) -> {
···
323
fn validate_single_property_schema(
324
prop_schema: Json,
325
ctx: ValidationContext,
326
-
) -> Result(Nil, ValidationError) {
327
dispatch_schema_validation(prop_schema, ctx)
328
}
329
···
333
properties: Json,
334
nullable_fields: List(String),
335
ctx: ValidationContext,
336
-
) -> Result(Nil, ValidationError) {
337
// Convert data to dict
338
case json_helpers.json_to_dict(data) {
339
Ok(data_dict) -> {
···
402
data: Json,
403
schema: Json,
404
ctx: ValidationContext,
405
-
) -> Result(Nil, ValidationError) {
406
dispatch_data_validation(data, schema, ctx)
407
}
408
···
418
pub fn validate_array_schema(
419
schema: Json,
420
ctx: ValidationContext,
421
-
) -> Result(Nil, ValidationError) {
422
let def_name = context.path(ctx)
423
424
// Validate allowed fields
···
465
data: Json,
466
schema: Json,
467
ctx: ValidationContext,
468
-
) -> Result(Nil, ValidationError) {
469
let def_name = context.path(ctx)
470
471
// Data must be an array
···
543
fn validate_array_item_schema(
544
items_schema: Json,
545
ctx: ValidationContext,
546
-
) -> Result(Nil, ValidationError) {
547
// Handle reference types by delegating to reference validator
548
case json_helpers.get_string(items_schema, "type") {
549
Some("ref") -> reference.validate_schema(items_schema, ctx)
···
556
item: Dynamic,
557
items_schema: Json,
558
ctx: ValidationContext,
559
-
) -> Result(Nil, ValidationError) {
560
// Convert dynamic to Json for validation
561
let item_json = json_helpers.dynamic_to_json(item)
562
···
1
// Field type validators (object and array)
2
3
+
import honk/errors as errors
4
import gleam/dict
5
import gleam/dynamic.{type Dynamic}
6
import gleam/dynamic/decode
···
11
import gleam/result
12
import honk/internal/constraints
13
import honk/internal/json_helpers
14
+
import honk/validation/context.{type ValidationContext}
15
16
// Import primitive validators
17
+
import honk/validation/primitive/blob
18
+
import honk/validation/primitive/boolean
19
+
import honk/validation/primitive/bytes
20
+
import honk/validation/primitive/cid_link
21
+
import honk/validation/primitive/integer
22
+
import honk/validation/primitive/null
23
+
import honk/validation/primitive/string
24
25
// Import other field validators
26
+
import honk/validation/field/reference
27
+
import honk/validation/field/union
28
29
// Import meta validators
30
+
import honk/validation/meta/token
31
+
import honk/validation/meta/unknown
32
33
// ============================================================================
34
// SHARED TYPE DISPATCHER
···
39
fn dispatch_schema_validation(
40
schema: Json,
41
ctx: ValidationContext,
42
+
) -> Result(Nil, errors.ValidationError) {
43
case json_helpers.get_string(schema, "type") {
44
Some("string") -> string.validate_schema(schema, ctx)
45
Some("integer") -> integer.validate_schema(schema, ctx)
···
91
data: Json,
92
schema: Json,
93
ctx: ValidationContext,
94
+
) -> Result(Nil, errors.ValidationError) {
95
case json_helpers.get_string(schema, "type") {
96
Some("string") -> string.validate_data(data, schema, ctx)
97
Some("integer") -> integer.validate_data(data, schema, ctx)
···
133
pub fn validate_object_schema(
134
schema: Json,
135
ctx: ValidationContext,
136
+
) -> Result(Nil, errors.ValidationError) {
137
let def_name = context.path(ctx)
138
139
// Validate allowed fields
···
192
data: Json,
193
schema: Json,
194
ctx: ValidationContext,
195
+
) -> Result(Nil, errors.ValidationError) {
196
let def_name = context.path(ctx)
197
198
// Check data is an object
···
235
fn validate_required_fields(
236
def_name: String,
237
required: List(Dynamic),
238
+
) -> Result(Nil, errors.ValidationError) {
239
// Convert dynamics to strings
240
let field_names =
241
list.filter_map(required, fn(item) { decode.run(item, decode.string) })
···
255
fn validate_nullable_fields(
256
def_name: String,
257
nullable: List(Dynamic),
258
+
) -> Result(Nil, errors.ValidationError) {
259
// Convert dynamics to strings
260
let field_names =
261
list.filter_map(nullable, fn(item) { decode.run(item, decode.string) })
···
276
def_name: String,
277
required: List(Dynamic),
278
data: Json,
279
+
) -> Result(Nil, errors.ValidationError) {
280
// Convert dynamics to strings
281
let field_names =
282
list.filter_map(required, fn(item) { decode.run(item, decode.string) })
···
299
fn validate_property_schemas(
300
properties: Json,
301
ctx: ValidationContext,
302
+
) -> Result(Nil, errors.ValidationError) {
303
// Convert JSON object to dict and validate each property
304
case json_helpers.json_to_dict(properties) {
305
Ok(props_dict) -> {
···
323
fn validate_single_property_schema(
324
prop_schema: Json,
325
ctx: ValidationContext,
326
+
) -> Result(Nil, errors.ValidationError) {
327
dispatch_schema_validation(prop_schema, ctx)
328
}
329
···
333
properties: Json,
334
nullable_fields: List(String),
335
ctx: ValidationContext,
336
+
) -> Result(Nil, errors.ValidationError) {
337
// Convert data to dict
338
case json_helpers.json_to_dict(data) {
339
Ok(data_dict) -> {
···
402
data: Json,
403
schema: Json,
404
ctx: ValidationContext,
405
+
) -> Result(Nil, errors.ValidationError) {
406
dispatch_data_validation(data, schema, ctx)
407
}
408
···
418
pub fn validate_array_schema(
419
schema: Json,
420
ctx: ValidationContext,
421
+
) -> Result(Nil, errors.ValidationError) {
422
let def_name = context.path(ctx)
423
424
// Validate allowed fields
···
465
data: Json,
466
schema: Json,
467
ctx: ValidationContext,
468
+
) -> Result(Nil, errors.ValidationError) {
469
let def_name = context.path(ctx)
470
471
// Data must be an array
···
543
fn validate_array_item_schema(
544
items_schema: Json,
545
ctx: ValidationContext,
546
+
) -> Result(Nil, errors.ValidationError) {
547
// Handle reference types by delegating to reference validator
548
case json_helpers.get_string(items_schema, "type") {
549
Some("ref") -> reference.validate_schema(items_schema, ctx)
···
556
item: Dynamic,
557
items_schema: Json,
558
ctx: ValidationContext,
559
+
) -> Result(Nil, errors.ValidationError) {
560
// Convert dynamic to Json for validation
561
let item_json = json_helpers.dynamic_to_json(item)
562
+6
-6
src/validation/field/reference.gleam
src/honk/validation/field/reference.gleam
+6
-6
src/validation/field/reference.gleam
src/honk/validation/field/reference.gleam
···
1
// Reference type validator
2
3
-
import errors.{type ValidationError}
4
import gleam/json.{type Json}
5
import gleam/option.{None, Some}
6
import gleam/result
···
8
import honk/internal/constraints
9
import honk/internal/json_helpers
10
import honk/internal/resolution
11
-
import validation/context.{type ValidationContext}
12
13
const allowed_fields = ["type", "ref", "description"]
14
···
16
pub fn validate_schema(
17
schema: Json,
18
ctx: ValidationContext,
19
-
) -> Result(Nil, ValidationError) {
20
let def_name = context.path(ctx)
21
22
// Validate allowed fields
···
49
data: Json,
50
schema: Json,
51
ctx: ValidationContext,
52
-
) -> Result(Nil, ValidationError) {
53
let def_name = context.path(ctx)
54
55
// Get the reference string
···
107
fn validate_ref_syntax(
108
ref_str: String,
109
def_name: String,
110
-
) -> Result(Nil, ValidationError) {
111
case string.is_empty(ref_str) {
112
True ->
113
Error(errors.invalid_schema(def_name <> ": reference cannot be empty"))
···
148
fn validate_global_ref_with_fragment(
149
ref_str: String,
150
def_name: String,
151
-
) -> Result(Nil, ValidationError) {
152
// Split on # and validate both parts
153
case string.split(ref_str, "#") {
154
[nsid, definition] -> {
···
1
// Reference type validator
2
3
+
import honk/errors as errors
4
import gleam/json.{type Json}
5
import gleam/option.{None, Some}
6
import gleam/result
···
8
import honk/internal/constraints
9
import honk/internal/json_helpers
10
import honk/internal/resolution
11
+
import honk/validation/context.{type ValidationContext}
12
13
const allowed_fields = ["type", "ref", "description"]
14
···
16
pub fn validate_schema(
17
schema: Json,
18
ctx: ValidationContext,
19
+
) -> Result(Nil, errors.ValidationError) {
20
let def_name = context.path(ctx)
21
22
// Validate allowed fields
···
49
data: Json,
50
schema: Json,
51
ctx: ValidationContext,
52
+
) -> Result(Nil, errors.ValidationError) {
53
let def_name = context.path(ctx)
54
55
// Get the reference string
···
107
fn validate_ref_syntax(
108
ref_str: String,
109
def_name: String,
110
+
) -> Result(Nil, errors.ValidationError) {
111
case string.is_empty(ref_str) {
112
True ->
113
Error(errors.invalid_schema(def_name <> ": reference cannot be empty"))
···
148
fn validate_global_ref_with_fragment(
149
ref_str: String,
150
def_name: String,
151
+
) -> Result(Nil, errors.ValidationError) {
152
// Split on # and validate both parts
153
case string.split(ref_str, "#") {
154
[nsid, definition] -> {
+4
-4
src/validation/field/union.gleam
src/honk/validation/field/union.gleam
+4
-4
src/validation/field/union.gleam
src/honk/validation/field/union.gleam
···
1
// Union type validator
2
3
-
import errors.{type ValidationError}
4
import gleam/dynamic/decode
5
import gleam/json.{type Json}
6
import gleam/list
···
9
import gleam/string
10
import honk/internal/constraints
11
import honk/internal/json_helpers
12
-
import validation/context.{type ValidationContext}
13
14
const allowed_fields = ["type", "refs", "closed", "description"]
15
···
17
pub fn validate_schema(
18
schema: Json,
19
ctx: ValidationContext,
20
-
) -> Result(Nil, ValidationError) {
21
let def_name = context.path(ctx)
22
23
// Validate allowed fields
···
90
data: Json,
91
schema: Json,
92
ctx: ValidationContext,
93
-
) -> Result(Nil, ValidationError) {
94
let def_name = context.path(ctx)
95
96
// Union data must be an object
···
1
// Union type validator
2
3
+
import honk/errors as errors
4
import gleam/dynamic/decode
5
import gleam/json.{type Json}
6
import gleam/list
···
9
import gleam/string
10
import honk/internal/constraints
11
import honk/internal/json_helpers
12
+
import honk/validation/context.{type ValidationContext}
13
14
const allowed_fields = ["type", "refs", "closed", "description"]
15
···
17
pub fn validate_schema(
18
schema: Json,
19
ctx: ValidationContext,
20
+
) -> Result(Nil, errors.ValidationError) {
21
let def_name = context.path(ctx)
22
23
// Validate allowed fields
···
90
data: Json,
91
schema: Json,
92
ctx: ValidationContext,
93
+
) -> Result(Nil, errors.ValidationError) {
94
let def_name = context.path(ctx)
95
96
// Union data must be an object
+2
-2
src/validation/formats.gleam
src/honk/validation/formats.gleam
+2
-2
src/validation/formats.gleam
src/honk/validation/formats.gleam
···
4
import gleam/regexp
5
import gleam/string
6
import gleam/time/timestamp
7
-
import types.{type StringFormat}
8
9
/// Validates RFC3339 datetime format
10
pub fn is_valid_rfc3339_datetime(value: String) -> Bool {
···
280
}
281
282
/// Validates a string value against a specific format
283
-
pub fn validate_format(value: String, format: StringFormat) -> Bool {
284
case format {
285
types.DateTime -> is_valid_rfc3339_datetime(value)
286
types.Uri -> is_valid_uri(value)
···
4
import gleam/regexp
5
import gleam/string
6
import gleam/time/timestamp
7
+
import honk/types as types
8
9
/// Validates RFC3339 datetime format
10
pub fn is_valid_rfc3339_datetime(value: String) -> Bool {
···
280
}
281
282
/// Validates a string value against a specific format
283
+
pub fn validate_format(value: String, format: types.StringFormat) -> Bool {
284
case format {
285
types.DateTime -> is_valid_rfc3339_datetime(value)
286
types.Uri -> is_valid_uri(value)
+4
-4
src/validation/meta/token.gleam
src/honk/validation/meta/token.gleam
+4
-4
src/validation/meta/token.gleam
src/honk/validation/meta/token.gleam
···
1
// Token type validator
2
// Tokens are unit types used for discrimination in unions
3
4
-
import errors.{type ValidationError}
5
import gleam/json.{type Json}
6
import gleam/string
7
import honk/internal/constraints
8
import honk/internal/json_helpers
9
-
import validation/context.{type ValidationContext}
10
11
const allowed_fields = ["type", "description"]
12
···
14
pub fn validate_schema(
15
schema: Json,
16
ctx: ValidationContext,
17
-
) -> Result(Nil, ValidationError) {
18
let def_name = context.path(ctx)
19
20
// Validate allowed fields
···
31
data: Json,
32
_schema: Json,
33
ctx: ValidationContext,
34
-
) -> Result(Nil, ValidationError) {
35
let def_name = context.path(ctx)
36
37
// Token data must be a string (the fully-qualified token name)
···
1
// Token type validator
2
// Tokens are unit types used for discrimination in unions
3
4
+
import honk/errors as errors
5
import gleam/json.{type Json}
6
import gleam/string
7
import honk/internal/constraints
8
import honk/internal/json_helpers
9
+
import honk/validation/context.{type ValidationContext}
10
11
const allowed_fields = ["type", "description"]
12
···
14
pub fn validate_schema(
15
schema: Json,
16
ctx: ValidationContext,
17
+
) -> Result(Nil, errors.ValidationError) {
18
let def_name = context.path(ctx)
19
20
// Validate allowed fields
···
31
data: Json,
32
_schema: Json,
33
ctx: ValidationContext,
34
+
) -> Result(Nil, errors.ValidationError) {
35
let def_name = context.path(ctx)
36
37
// Token data must be a string (the fully-qualified token name)
+4
-4
src/validation/meta/unknown.gleam
src/honk/validation/meta/unknown.gleam
+4
-4
src/validation/meta/unknown.gleam
src/honk/validation/meta/unknown.gleam
···
1
// Unknown type validator
2
// Unknown allows flexible data with AT Protocol data model rules
3
4
-
import errors.{type ValidationError}
5
import gleam/json.{type Json}
6
import gleam/option.{None, Some}
7
import honk/internal/constraints
8
import honk/internal/json_helpers
9
-
import validation/context.{type ValidationContext}
10
11
const allowed_fields = ["type", "description"]
12
···
14
pub fn validate_schema(
15
schema: Json,
16
ctx: ValidationContext,
17
-
) -> Result(Nil, ValidationError) {
18
let def_name = context.path(ctx)
19
20
// Validate allowed fields
···
28
data: Json,
29
_schema: Json,
30
ctx: ValidationContext,
31
-
) -> Result(Nil, ValidationError) {
32
let def_name = context.path(ctx)
33
34
// Unknown data must be an object (not primitives, arrays, bytes, or blobs)
···
1
// Unknown type validator
2
// Unknown allows flexible data with AT Protocol data model rules
3
4
+
import honk/errors as errors
5
import gleam/json.{type Json}
6
import gleam/option.{None, Some}
7
import honk/internal/constraints
8
import honk/internal/json_helpers
9
+
import honk/validation/context.{type ValidationContext}
10
11
const allowed_fields = ["type", "description"]
12
···
14
pub fn validate_schema(
15
schema: Json,
16
ctx: ValidationContext,
17
+
) -> Result(Nil, errors.ValidationError) {
18
let def_name = context.path(ctx)
19
20
// Validate allowed fields
···
28
data: Json,
29
_schema: Json,
30
ctx: ValidationContext,
31
+
) -> Result(Nil, errors.ValidationError) {
32
let def_name = context.path(ctx)
33
34
// Unknown data must be an object (not primitives, arrays, bytes, or blobs)
+13
-13
src/validation/primary/params.gleam
src/honk/validation/primary/params.gleam
+13
-13
src/validation/primary/params.gleam
src/honk/validation/primary/params.gleam
···
2
// Mirrors the Go implementation's validation/primary/params
3
// Params define query/procedure/subscription parameters (XRPC endpoint arguments)
4
5
-
import errors.{type ValidationError}
6
import gleam/dynamic/decode
7
import gleam/json.{type Json}
8
import gleam/list
···
10
import gleam/result
11
import honk/internal/constraints
12
import honk/internal/json_helpers
13
-
import validation/context.{type ValidationContext}
14
-
import validation/field as validation_field
15
-
import validation/meta/unknown as validation_meta_unknown
16
-
import validation/primitive/boolean as validation_primitive_boolean
17
-
import validation/primitive/integer as validation_primitive_integer
18
-
import validation/primitive/string as validation_primitive_string
19
20
const allowed_fields = ["type", "description", "properties", "required"]
21
···
23
pub fn validate_schema(
24
schema: Json,
25
ctx: ValidationContext,
26
-
) -> Result(Nil, ValidationError) {
27
let def_name = context.path(ctx)
28
29
// Validate allowed fields
···
74
def_name: String,
75
required_array: option.Option(List(decode.Dynamic)),
76
properties_dict: json_helpers.JsonDict,
77
-
) -> Result(Nil, ValidationError) {
78
case required_array {
79
None -> Ok(Nil)
80
Some(required) -> {
···
107
def_name: String,
108
properties_dict: json_helpers.JsonDict,
109
ctx: ValidationContext,
110
-
) -> Result(Nil, ValidationError) {
111
json_helpers.dict_fold(properties_dict, Ok(Nil), fn(acc, key, value) {
112
case acc {
113
Error(e) -> Error(e)
···
144
property_name: String,
145
property_schema: Json,
146
ctx: ValidationContext,
147
-
) -> Result(Nil, ValidationError) {
148
let prop_path = def_name <> ".properties." <> property_name
149
150
case json_helpers.get_string(property_schema, "type") {
···
199
fn validate_property_schema(
200
schema: Json,
201
ctx: ValidationContext,
202
-
) -> Result(Nil, ValidationError) {
203
case json_helpers.get_string(schema, "type") {
204
Some("boolean") -> validation_primitive_boolean.validate_schema(schema, ctx)
205
Some("integer") -> validation_primitive_integer.validate_schema(schema, ctx)
···
222
_data: Json,
223
_schema: Json,
224
_ctx: ValidationContext,
225
-
) -> Result(Nil, ValidationError) {
226
// Params data validation would check that all required parameters are present
227
// and that each parameter value matches its schema
228
// For now, simplified implementation
···
2
// Mirrors the Go implementation's validation/primary/params
3
// Params define query/procedure/subscription parameters (XRPC endpoint arguments)
4
5
+
import honk/errors as errors
6
import gleam/dynamic/decode
7
import gleam/json.{type Json}
8
import gleam/list
···
10
import gleam/result
11
import honk/internal/constraints
12
import honk/internal/json_helpers
13
+
import honk/validation/context.{type ValidationContext}
14
+
import honk/validation/field as validation_field
15
+
import honk/validation/meta/unknown as validation_meta_unknown
16
+
import honk/validation/primitive/boolean as validation_primitive_boolean
17
+
import honk/validation/primitive/integer as validation_primitive_integer
18
+
import honk/validation/primitive/string as validation_primitive_string
19
20
const allowed_fields = ["type", "description", "properties", "required"]
21
···
23
pub fn validate_schema(
24
schema: Json,
25
ctx: ValidationContext,
26
+
) -> Result(Nil, errors.ValidationError) {
27
let def_name = context.path(ctx)
28
29
// Validate allowed fields
···
74
def_name: String,
75
required_array: option.Option(List(decode.Dynamic)),
76
properties_dict: json_helpers.JsonDict,
77
+
) -> Result(Nil, errors.ValidationError) {
78
case required_array {
79
None -> Ok(Nil)
80
Some(required) -> {
···
107
def_name: String,
108
properties_dict: json_helpers.JsonDict,
109
ctx: ValidationContext,
110
+
) -> Result(Nil, errors.ValidationError) {
111
json_helpers.dict_fold(properties_dict, Ok(Nil), fn(acc, key, value) {
112
case acc {
113
Error(e) -> Error(e)
···
144
property_name: String,
145
property_schema: Json,
146
ctx: ValidationContext,
147
+
) -> Result(Nil, errors.ValidationError) {
148
let prop_path = def_name <> ".properties." <> property_name
149
150
case json_helpers.get_string(property_schema, "type") {
···
199
fn validate_property_schema(
200
schema: Json,
201
ctx: ValidationContext,
202
+
) -> Result(Nil, errors.ValidationError) {
203
case json_helpers.get_string(schema, "type") {
204
Some("boolean") -> validation_primitive_boolean.validate_schema(schema, ctx)
205
Some("integer") -> validation_primitive_integer.validate_schema(schema, ctx)
···
222
_data: Json,
223
_schema: Json,
224
_ctx: ValidationContext,
225
+
) -> Result(Nil, errors.ValidationError) {
226
// Params data validation would check that all required parameters are present
227
// and that each parameter value matches its schema
228
// For now, simplified implementation
+13
-13
src/validation/primary/procedure.gleam
src/honk/validation/primary/procedure.gleam
+13
-13
src/validation/primary/procedure.gleam
src/honk/validation/primary/procedure.gleam
···
1
// Procedure type validator
2
// Procedures are XRPC Procedure (HTTP POST) endpoints for modifying data
3
4
-
import errors.{type ValidationError}
5
import gleam/json.{type Json}
6
import gleam/option.{None, Some}
7
import gleam/result
8
import honk/internal/constraints
9
import honk/internal/json_helpers
10
-
import validation/context.{type ValidationContext}
11
-
import validation/field as validation_field
12
-
import validation/field/reference as validation_field_reference
13
-
import validation/field/union as validation_field_union
14
-
import validation/primary/params
15
16
const allowed_fields = [
17
"type", "parameters", "input", "output", "errors", "description",
···
21
pub fn validate_schema(
22
schema: Json,
23
ctx: ValidationContext,
24
-
) -> Result(Nil, ValidationError) {
25
let def_name = context.path(ctx)
26
27
// Validate allowed fields
···
64
data: Json,
65
schema: Json,
66
ctx: ValidationContext,
67
-
) -> Result(Nil, ValidationError) {
68
// If schema has input, validate data against it
69
case json_helpers.get_field(schema, "input") {
70
Some(input) -> {
···
80
data: Json,
81
schema: Json,
82
ctx: ValidationContext,
83
-
) -> Result(Nil, ValidationError) {
84
// If schema has output, validate data against it
85
case json_helpers.get_field(schema, "output") {
86
Some(output) -> {
···
96
data: Json,
97
body: Json,
98
ctx: ValidationContext,
99
-
) -> Result(Nil, ValidationError) {
100
// Get the schema field from the body
101
case json_helpers.get_field(body, "schema") {
102
Some(schema) -> {
···
113
data: Json,
114
schema: Json,
115
ctx: ValidationContext,
116
-
) -> Result(Nil, ValidationError) {
117
case json_helpers.get_string(schema, "type") {
118
Some("object") -> validation_field.validate_object_data(data, schema, ctx)
119
Some("ref") -> {
···
140
fn validate_parameters_schema(
141
parameters: Json,
142
ctx: ValidationContext,
143
-
) -> Result(Nil, ValidationError) {
144
// Validate the full params schema
145
let params_ctx = context.with_path(ctx, "parameters")
146
params.validate_schema(parameters, params_ctx)
···
151
def_name: String,
152
io: Json,
153
field_name: String,
154
-
) -> Result(Nil, ValidationError) {
155
// Input/output must have encoding field
156
case json_helpers.get_string(io, "encoding") {
157
Some(_) -> Ok(Nil)
···
1
// Procedure type validator
2
// Procedures are XRPC Procedure (HTTP POST) endpoints for modifying data
3
4
+
import honk/errors as errors
5
import gleam/json.{type Json}
6
import gleam/option.{None, Some}
7
import gleam/result
8
import honk/internal/constraints
9
import honk/internal/json_helpers
10
+
import honk/validation/context.{type ValidationContext}
11
+
import honk/validation/field as validation_field
12
+
import honk/validation/field/reference as validation_field_reference
13
+
import honk/validation/field/union as validation_field_union
14
+
import honk/validation/primary/params
15
16
const allowed_fields = [
17
"type", "parameters", "input", "output", "errors", "description",
···
21
pub fn validate_schema(
22
schema: Json,
23
ctx: ValidationContext,
24
+
) -> Result(Nil, errors.ValidationError) {
25
let def_name = context.path(ctx)
26
27
// Validate allowed fields
···
64
data: Json,
65
schema: Json,
66
ctx: ValidationContext,
67
+
) -> Result(Nil, errors.ValidationError) {
68
// If schema has input, validate data against it
69
case json_helpers.get_field(schema, "input") {
70
Some(input) -> {
···
80
data: Json,
81
schema: Json,
82
ctx: ValidationContext,
83
+
) -> Result(Nil, errors.ValidationError) {
84
// If schema has output, validate data against it
85
case json_helpers.get_field(schema, "output") {
86
Some(output) -> {
···
96
data: Json,
97
body: Json,
98
ctx: ValidationContext,
99
+
) -> Result(Nil, errors.ValidationError) {
100
// Get the schema field from the body
101
case json_helpers.get_field(body, "schema") {
102
Some(schema) -> {
···
113
data: Json,
114
schema: Json,
115
ctx: ValidationContext,
116
+
) -> Result(Nil, errors.ValidationError) {
117
case json_helpers.get_string(schema, "type") {
118
Some("object") -> validation_field.validate_object_data(data, schema, ctx)
119
Some("ref") -> {
···
140
fn validate_parameters_schema(
141
parameters: Json,
142
ctx: ValidationContext,
143
+
) -> Result(Nil, errors.ValidationError) {
144
// Validate the full params schema
145
let params_ctx = context.with_path(ctx, "parameters")
146
params.validate_schema(parameters, params_ctx)
···
151
def_name: String,
152
io: Json,
153
field_name: String,
154
+
) -> Result(Nil, errors.ValidationError) {
155
// Input/output must have encoding field
156
case json_helpers.get_string(io, "encoding") {
157
Some(_) -> Ok(Nil)
+14
-14
src/validation/primary/query.gleam
src/honk/validation/primary/query.gleam
+14
-14
src/validation/primary/query.gleam
src/honk/validation/primary/query.gleam
···
1
// Query type validator
2
// Queries are XRPC Query (HTTP GET) endpoints for retrieving data
3
4
-
import errors.{type ValidationError}
5
import gleam/dynamic/decode
6
import gleam/json.{type Json}
7
import gleam/list
···
9
import gleam/result
10
import honk/internal/constraints
11
import honk/internal/json_helpers
12
-
import validation/context.{type ValidationContext}
13
-
import validation/field as validation_field
14
-
import validation/meta/unknown as validation_meta_unknown
15
-
import validation/primary/params
16
-
import validation/primitive/boolean as validation_primitive_boolean
17
-
import validation/primitive/integer as validation_primitive_integer
18
-
import validation/primitive/string as validation_primitive_string
19
20
const allowed_fields = ["type", "parameters", "output", "errors", "description"]
21
···
23
pub fn validate_schema(
24
schema: Json,
25
ctx: ValidationContext,
26
-
) -> Result(Nil, ValidationError) {
27
let def_name = context.path(ctx)
28
29
// Validate allowed fields
···
60
data: Json,
61
schema: Json,
62
ctx: ValidationContext,
63
-
) -> Result(Nil, ValidationError) {
64
let def_name = context.path(ctx)
65
66
// Query data must be an object (the parameters)
···
87
data: Json,
88
params_schema: Json,
89
ctx: ValidationContext,
90
-
) -> Result(Nil, ValidationError) {
91
let def_name = context.path(ctx)
92
93
// Get data as dict
···
173
value: Json,
174
schema: Json,
175
ctx: ValidationContext,
176
-
) -> Result(Nil, ValidationError) {
177
// Dispatch based on schema type
178
case json_helpers.get_string(schema, "type") {
179
Some("boolean") ->
···
202
fn validate_parameters_schema(
203
parameters: Json,
204
ctx: ValidationContext,
205
-
) -> Result(Nil, ValidationError) {
206
// Validate the full params schema
207
let params_ctx = context.with_path(ctx, "parameters")
208
params.validate_schema(parameters, params_ctx)
···
212
fn validate_output_schema(
213
def_name: String,
214
output: Json,
215
-
) -> Result(Nil, ValidationError) {
216
// Output must have encoding field
217
case json_helpers.get_string(output, "encoding") {
218
Some(_) -> Ok(Nil)
···
1
// Query type validator
2
// Queries are XRPC Query (HTTP GET) endpoints for retrieving data
3
4
+
import honk/errors as errors
5
import gleam/dynamic/decode
6
import gleam/json.{type Json}
7
import gleam/list
···
9
import gleam/result
10
import honk/internal/constraints
11
import honk/internal/json_helpers
12
+
import honk/validation/context.{type ValidationContext}
13
+
import honk/validation/field as validation_field
14
+
import honk/validation/meta/unknown as validation_meta_unknown
15
+
import honk/validation/primary/params
16
+
import honk/validation/primitive/boolean as validation_primitive_boolean
17
+
import honk/validation/primitive/integer as validation_primitive_integer
18
+
import honk/validation/primitive/string as validation_primitive_string
19
20
const allowed_fields = ["type", "parameters", "output", "errors", "description"]
21
···
23
pub fn validate_schema(
24
schema: Json,
25
ctx: ValidationContext,
26
+
) -> Result(Nil, errors.ValidationError) {
27
let def_name = context.path(ctx)
28
29
// Validate allowed fields
···
60
data: Json,
61
schema: Json,
62
ctx: ValidationContext,
63
+
) -> Result(Nil, errors.ValidationError) {
64
let def_name = context.path(ctx)
65
66
// Query data must be an object (the parameters)
···
87
data: Json,
88
params_schema: Json,
89
ctx: ValidationContext,
90
+
) -> Result(Nil, errors.ValidationError) {
91
let def_name = context.path(ctx)
92
93
// Get data as dict
···
173
value: Json,
174
schema: Json,
175
ctx: ValidationContext,
176
+
) -> Result(Nil, errors.ValidationError) {
177
// Dispatch based on schema type
178
case json_helpers.get_string(schema, "type") {
179
Some("boolean") ->
···
202
fn validate_parameters_schema(
203
parameters: Json,
204
ctx: ValidationContext,
205
+
) -> Result(Nil, errors.ValidationError) {
206
// Validate the full params schema
207
let params_ctx = context.with_path(ctx, "parameters")
208
params.validate_schema(parameters, params_ctx)
···
212
fn validate_output_schema(
213
def_name: String,
214
output: Json,
215
+
) -> Result(Nil, errors.ValidationError) {
216
// Output must have encoding field
217
case json_helpers.get_string(output, "encoding") {
218
Some(_) -> Ok(Nil)
+7
-7
src/validation/primary/record.gleam
src/honk/validation/primary/record.gleam
+7
-7
src/validation/primary/record.gleam
src/honk/validation/primary/record.gleam
···
1
// Record type validator
2
3
-
import errors.{type ValidationError}
4
import gleam/json.{type Json}
5
import gleam/option.{None, Some}
6
import gleam/result
7
import gleam/string
8
import honk/internal/constraints
9
import honk/internal/json_helpers
10
-
import validation/context.{type ValidationContext}
11
-
import validation/field
12
13
const allowed_fields = ["type", "key", "record", "description"]
14
···
20
pub fn validate_schema(
21
schema: Json,
22
ctx: ValidationContext,
23
-
) -> Result(Nil, ValidationError) {
24
let def_name = context.path(ctx)
25
26
// Validate allowed fields at record level
···
69
data: Json,
70
schema: Json,
71
ctx: ValidationContext,
72
-
) -> Result(Nil, ValidationError) {
73
let def_name = context.path(ctx)
74
75
// Data must be an object
···
101
/// - `any`: Record key can be any valid record key format
102
/// - `nsid`: Record key must be a valid NSID
103
/// - `literal:*`: Record key must match the literal value after the colon
104
-
fn validate_key(def_name: String, key: String) -> Result(Nil, ValidationError) {
105
case key {
106
"tid" -> Ok(Nil)
107
"any" -> Ok(Nil)
···
124
fn validate_record_object(
125
def_name: String,
126
record_def: Json,
127
-
) -> Result(Nil, ValidationError) {
128
// Must be type "object"
129
case json_helpers.get_string(record_def, "type") {
130
Some("object") -> {
···
1
// Record type validator
2
3
+
import honk/errors as errors
4
import gleam/json.{type Json}
5
import gleam/option.{None, Some}
6
import gleam/result
7
import gleam/string
8
import honk/internal/constraints
9
import honk/internal/json_helpers
10
+
import honk/validation/context.{type ValidationContext}
11
+
import honk/validation/field
12
13
const allowed_fields = ["type", "key", "record", "description"]
14
···
20
pub fn validate_schema(
21
schema: Json,
22
ctx: ValidationContext,
23
+
) -> Result(Nil, errors.ValidationError) {
24
let def_name = context.path(ctx)
25
26
// Validate allowed fields at record level
···
69
data: Json,
70
schema: Json,
71
ctx: ValidationContext,
72
+
) -> Result(Nil, errors.ValidationError) {
73
let def_name = context.path(ctx)
74
75
// Data must be an object
···
101
/// - `any`: Record key can be any valid record key format
102
/// - `nsid`: Record key must be a valid NSID
103
/// - `literal:*`: Record key must match the literal value after the colon
104
+
fn validate_key(def_name: String, key: String) -> Result(Nil, errors.ValidationError) {
105
case key {
106
"tid" -> Ok(Nil)
107
"any" -> Ok(Nil)
···
124
fn validate_record_object(
125
def_name: String,
126
record_def: Json,
127
+
) -> Result(Nil, errors.ValidationError) {
128
// Must be type "object"
129
case json_helpers.get_string(record_def, "type") {
130
Some("object") -> {
+16
-16
src/validation/primary/subscription.gleam
src/honk/validation/primary/subscription.gleam
+16
-16
src/validation/primary/subscription.gleam
src/honk/validation/primary/subscription.gleam
···
1
// Subscription type validator
2
// Subscriptions are XRPC Subscription (WebSocket) endpoints for real-time data
3
4
-
import errors.{type ValidationError}
5
import gleam/dynamic/decode
6
import gleam/json.{type Json}
7
import gleam/list
···
9
import gleam/result
10
import honk/internal/constraints
11
import honk/internal/json_helpers
12
-
import validation/context.{type ValidationContext}
13
-
import validation/field as validation_field
14
-
import validation/field/union as validation_field_union
15
-
import validation/meta/unknown as validation_meta_unknown
16
-
import validation/primary/params
17
-
import validation/primitive/boolean as validation_primitive_boolean
18
-
import validation/primitive/integer as validation_primitive_integer
19
-
import validation/primitive/string as validation_primitive_string
20
21
const allowed_fields = [
22
"type",
···
30
pub fn validate_schema(
31
schema: Json,
32
ctx: ValidationContext,
33
-
) -> Result(Nil, ValidationError) {
34
let def_name = context.path(ctx)
35
36
// Validate allowed fields
···
67
data: Json,
68
schema: Json,
69
ctx: ValidationContext,
70
-
) -> Result(Nil, ValidationError) {
71
let def_name = context.path(ctx)
72
73
// Subscription parameter data must be an object
···
94
data: Json,
95
schema: Json,
96
ctx: ValidationContext,
97
-
) -> Result(Nil, ValidationError) {
98
// Get the message schema
99
case json_helpers.get_field(schema, "message") {
100
Some(message) -> {
···
117
data: Json,
118
params_schema: Json,
119
ctx: ValidationContext,
120
-
) -> Result(Nil, ValidationError) {
121
let def_name = context.path(ctx)
122
123
// Get data as dict
···
202
value: Json,
203
schema: Json,
204
ctx: ValidationContext,
205
-
) -> Result(Nil, ValidationError) {
206
// Dispatch based on schema type
207
case json_helpers.get_string(schema, "type") {
208
Some("boolean") ->
···
231
fn validate_parameters_schema(
232
parameters: Json,
233
ctx: ValidationContext,
234
-
) -> Result(Nil, ValidationError) {
235
// Validate the full params schema
236
let params_ctx = context.with_path(ctx, "parameters")
237
params.validate_schema(parameters, params_ctx)
···
241
fn validate_message_schema(
242
def_name: String,
243
message: Json,
244
-
) -> Result(Nil, ValidationError) {
245
// Message must have schema field
246
case json_helpers.get_field(message, "schema") {
247
Some(schema_field) -> {
···
1
// Subscription type validator
2
// Subscriptions are XRPC Subscription (WebSocket) endpoints for real-time data
3
4
+
import honk/errors as errors
5
import gleam/dynamic/decode
6
import gleam/json.{type Json}
7
import gleam/list
···
9
import gleam/result
10
import honk/internal/constraints
11
import honk/internal/json_helpers
12
+
import honk/validation/context.{type ValidationContext}
13
+
import honk/validation/field as validation_field
14
+
import honk/validation/field/union as validation_field_union
15
+
import honk/validation/meta/unknown as validation_meta_unknown
16
+
import honk/validation/primary/params
17
+
import honk/validation/primitive/boolean as validation_primitive_boolean
18
+
import honk/validation/primitive/integer as validation_primitive_integer
19
+
import honk/validation/primitive/string as validation_primitive_string
20
21
const allowed_fields = [
22
"type",
···
30
pub fn validate_schema(
31
schema: Json,
32
ctx: ValidationContext,
33
+
) -> Result(Nil, errors.ValidationError) {
34
let def_name = context.path(ctx)
35
36
// Validate allowed fields
···
67
data: Json,
68
schema: Json,
69
ctx: ValidationContext,
70
+
) -> Result(Nil, errors.ValidationError) {
71
let def_name = context.path(ctx)
72
73
// Subscription parameter data must be an object
···
94
data: Json,
95
schema: Json,
96
ctx: ValidationContext,
97
+
) -> Result(Nil, errors.ValidationError) {
98
// Get the message schema
99
case json_helpers.get_field(schema, "message") {
100
Some(message) -> {
···
117
data: Json,
118
params_schema: Json,
119
ctx: ValidationContext,
120
+
) -> Result(Nil, errors.ValidationError) {
121
let def_name = context.path(ctx)
122
123
// Get data as dict
···
202
value: Json,
203
schema: Json,
204
ctx: ValidationContext,
205
+
) -> Result(Nil, errors.ValidationError) {
206
// Dispatch based on schema type
207
case json_helpers.get_string(schema, "type") {
208
Some("boolean") ->
···
231
fn validate_parameters_schema(
232
parameters: Json,
233
ctx: ValidationContext,
234
+
) -> Result(Nil, errors.ValidationError) {
235
// Validate the full params schema
236
let params_ctx = context.with_path(ctx, "parameters")
237
params.validate_schema(parameters, params_ctx)
···
241
fn validate_message_schema(
242
def_name: String,
243
message: Json,
244
+
) -> Result(Nil, errors.ValidationError) {
245
// Message must have schema field
246
case json_helpers.get_field(message, "schema") {
247
Some(schema_field) -> {
+8
-8
src/validation/primitive/blob.gleam
src/honk/validation/primitive/blob.gleam
+8
-8
src/validation/primitive/blob.gleam
src/honk/validation/primitive/blob.gleam
···
1
// Blob type validator
2
// Blobs are binary objects with MIME types and size constraints
3
4
-
import errors.{type ValidationError}
5
import gleam/dynamic.{type Dynamic}
6
import gleam/dynamic/decode
7
import gleam/int
···
12
import gleam/string
13
import honk/internal/constraints
14
import honk/internal/json_helpers
15
-
import validation/context.{type ValidationContext}
16
17
const allowed_fields = ["type", "accept", "maxSize", "description"]
18
···
20
pub fn validate_schema(
21
schema: Json,
22
ctx: ValidationContext,
23
-
) -> Result(Nil, ValidationError) {
24
let def_name = context.path(ctx)
25
26
// Validate allowed fields
···
57
data: Json,
58
schema: Json,
59
ctx: ValidationContext,
60
-
) -> Result(Nil, ValidationError) {
61
let def_name = context.path(ctx)
62
63
// Data must be an object
···
118
fn validate_accept_field(
119
def_name: String,
120
accept_array: List(Dynamic),
121
-
) -> Result(Nil, ValidationError) {
122
list.index_fold(accept_array, Ok(Nil), fn(acc, item, i) {
123
use _ <- result.try(acc)
124
case decode.run(item, decode.string) {
···
139
def_name: String,
140
mime_type: String,
141
_index: Int,
142
-
) -> Result(Nil, ValidationError) {
143
case string.is_empty(mime_type) {
144
True ->
145
Error(errors.invalid_schema(
···
199
part: String,
200
part_name: String,
201
full_mime_type: String,
202
-
) -> Result(Nil, ValidationError) {
203
case string.contains(part, "*") {
204
True ->
205
case part {
···
222
def_name: String,
223
mime_type: String,
224
accept_array: List(Dynamic),
225
-
) -> Result(Nil, ValidationError) {
226
let accept_patterns =
227
list.filter_map(accept_array, fn(item) { decode.run(item, decode.string) })
228
···
1
// Blob type validator
2
// Blobs are binary objects with MIME types and size constraints
3
4
+
import honk/errors as errors
5
import gleam/dynamic.{type Dynamic}
6
import gleam/dynamic/decode
7
import gleam/int
···
12
import gleam/string
13
import honk/internal/constraints
14
import honk/internal/json_helpers
15
+
import honk/validation/context.{type ValidationContext}
16
17
const allowed_fields = ["type", "accept", "maxSize", "description"]
18
···
20
pub fn validate_schema(
21
schema: Json,
22
ctx: ValidationContext,
23
+
) -> Result(Nil, errors.ValidationError) {
24
let def_name = context.path(ctx)
25
26
// Validate allowed fields
···
57
data: Json,
58
schema: Json,
59
ctx: ValidationContext,
60
+
) -> Result(Nil, errors.ValidationError) {
61
let def_name = context.path(ctx)
62
63
// Data must be an object
···
118
fn validate_accept_field(
119
def_name: String,
120
accept_array: List(Dynamic),
121
+
) -> Result(Nil, errors.ValidationError) {
122
list.index_fold(accept_array, Ok(Nil), fn(acc, item, i) {
123
use _ <- result.try(acc)
124
case decode.run(item, decode.string) {
···
139
def_name: String,
140
mime_type: String,
141
_index: Int,
142
+
) -> Result(Nil, errors.ValidationError) {
143
case string.is_empty(mime_type) {
144
True ->
145
Error(errors.invalid_schema(
···
199
part: String,
200
part_name: String,
201
full_mime_type: String,
202
+
) -> Result(Nil, errors.ValidationError) {
203
case string.contains(part, "*") {
204
True ->
205
case part {
···
222
def_name: String,
223
mime_type: String,
224
accept_array: List(Dynamic),
225
+
) -> Result(Nil, errors.ValidationError) {
226
let accept_patterns =
227
list.filter_map(accept_array, fn(item) { decode.run(item, decode.string) })
228
+4
-4
src/validation/primitive/boolean.gleam
src/honk/validation/primitive/boolean.gleam
+4
-4
src/validation/primitive/boolean.gleam
src/honk/validation/primitive/boolean.gleam
···
1
// Boolean type validator
2
3
-
import errors.{type ValidationError}
4
import gleam/json.{type Json}
5
import gleam/option.{None, Some}
6
import gleam/result
7
import honk/internal/constraints
8
import honk/internal/json_helpers
9
-
import validation/context.{type ValidationContext}
10
11
const allowed_fields = ["type", "const", "default", "description"]
12
···
14
pub fn validate_schema(
15
schema: Json,
16
ctx: ValidationContext,
17
-
) -> Result(Nil, ValidationError) {
18
let def_name = context.path(ctx)
19
20
// Validate allowed fields
···
43
data: Json,
44
schema: Json,
45
ctx: ValidationContext,
46
-
) -> Result(Nil, ValidationError) {
47
let def_name = context.path(ctx)
48
49
// Check data is a boolean
···
1
// Boolean type validator
2
3
+
import honk/errors as errors
4
import gleam/json.{type Json}
5
import gleam/option.{None, Some}
6
import gleam/result
7
import honk/internal/constraints
8
import honk/internal/json_helpers
9
+
import honk/validation/context.{type ValidationContext}
10
11
const allowed_fields = ["type", "const", "default", "description"]
12
···
14
pub fn validate_schema(
15
schema: Json,
16
ctx: ValidationContext,
17
+
) -> Result(Nil, errors.ValidationError) {
18
let def_name = context.path(ctx)
19
20
// Validate allowed fields
···
43
data: Json,
44
schema: Json,
45
ctx: ValidationContext,
46
+
) -> Result(Nil, errors.ValidationError) {
47
let def_name = context.path(ctx)
48
49
// Check data is a boolean
+4
-4
src/validation/primitive/bytes.gleam
src/honk/validation/primitive/bytes.gleam
+4
-4
src/validation/primitive/bytes.gleam
src/honk/validation/primitive/bytes.gleam
···
1
// Bytes type validator
2
// Bytes are base64-encoded strings
3
4
-
import errors.{type ValidationError}
5
import gleam/bit_array
6
import gleam/json.{type Json}
7
import gleam/list
···
10
import gleam/string
11
import honk/internal/constraints
12
import honk/internal/json_helpers
13
-
import validation/context.{type ValidationContext}
14
15
const allowed_fields = ["type", "minLength", "maxLength", "description"]
16
···
18
pub fn validate_schema(
19
schema: Json,
20
ctx: ValidationContext,
21
-
) -> Result(Nil, ValidationError) {
22
let def_name = context.path(ctx)
23
24
// Validate allowed fields
···
65
data: Json,
66
schema: Json,
67
ctx: ValidationContext,
68
-
) -> Result(Nil, ValidationError) {
69
let def_name = context.path(ctx)
70
71
// Check data is an object
···
1
// Bytes type validator
2
// Bytes are base64-encoded strings
3
4
+
import honk/errors as errors
5
import gleam/bit_array
6
import gleam/json.{type Json}
7
import gleam/list
···
10
import gleam/string
11
import honk/internal/constraints
12
import honk/internal/json_helpers
13
+
import honk/validation/context.{type ValidationContext}
14
15
const allowed_fields = ["type", "minLength", "maxLength", "description"]
16
···
18
pub fn validate_schema(
19
schema: Json,
20
ctx: ValidationContext,
21
+
) -> Result(Nil, errors.ValidationError) {
22
let def_name = context.path(ctx)
23
24
// Validate allowed fields
···
65
data: Json,
66
schema: Json,
67
ctx: ValidationContext,
68
+
) -> Result(Nil, errors.ValidationError) {
69
let def_name = context.path(ctx)
70
71
// Check data is an object
+5
-5
src/validation/primitive/cid_link.gleam
src/honk/validation/primitive/cid_link.gleam
+5
-5
src/validation/primitive/cid_link.gleam
src/honk/validation/primitive/cid_link.gleam
···
1
// CID Link type validator
2
// CID links are IPFS content identifiers
3
4
-
import errors.{type ValidationError}
5
import gleam/json.{type Json}
6
import gleam/option
7
import honk/internal/constraints
8
import honk/internal/json_helpers
9
-
import validation/context.{type ValidationContext}
10
-
import validation/formats
11
12
const allowed_fields = ["type", "description"]
13
···
15
pub fn validate_schema(
16
schema: Json,
17
ctx: ValidationContext,
18
-
) -> Result(Nil, ValidationError) {
19
let def_name = context.path(ctx)
20
21
// Validate allowed fields
···
33
data: Json,
34
_schema: Json,
35
ctx: ValidationContext,
36
-
) -> Result(Nil, ValidationError) {
37
let def_name = context.path(ctx)
38
39
// Check data is an object with $link field
···
1
// CID Link type validator
2
// CID links are IPFS content identifiers
3
4
+
import honk/errors as errors
5
import gleam/json.{type Json}
6
import gleam/option
7
import honk/internal/constraints
8
import honk/internal/json_helpers
9
+
import honk/validation/context.{type ValidationContext}
10
+
import honk/validation/formats
11
12
const allowed_fields = ["type", "description"]
13
···
15
pub fn validate_schema(
16
schema: Json,
17
ctx: ValidationContext,
18
+
) -> Result(Nil, errors.ValidationError) {
19
let def_name = context.path(ctx)
20
21
// Validate allowed fields
···
33
data: Json,
34
_schema: Json,
35
ctx: ValidationContext,
36
+
) -> Result(Nil, errors.ValidationError) {
37
let def_name = context.path(ctx)
38
39
// Check data is an object with $link field
+5
-5
src/validation/primitive/integer.gleam
src/honk/validation/primitive/integer.gleam
+5
-5
src/validation/primitive/integer.gleam
src/honk/validation/primitive/integer.gleam
···
1
// Integer type validator
2
3
-
import errors.{type ValidationError}
4
import gleam/dynamic/decode
5
import gleam/int
6
import gleam/json.{type Json}
···
9
import gleam/result
10
import honk/internal/constraints
11
import honk/internal/json_helpers
12
-
import validation/context.{type ValidationContext}
13
14
const allowed_fields = [
15
"type", "minimum", "maximum", "enum", "const", "default", "description",
···
19
pub fn validate_schema(
20
schema: Json,
21
ctx: ValidationContext,
22
-
) -> Result(Nil, ValidationError) {
23
let def_name = context.path(ctx)
24
25
// Validate allowed fields
···
75
data: Json,
76
schema: Json,
77
ctx: ValidationContext,
78
-
) -> Result(Nil, ValidationError) {
79
let def_name = context.path(ctx)
80
81
// Check data is an integer
···
141
value: Int,
142
enum_values: List(Int),
143
def_name: String,
144
-
) -> Result(Nil, ValidationError) {
145
constraints.validate_enum_constraint(
146
def_name,
147
value,
···
1
// Integer type validator
2
3
+
import honk/errors as errors
4
import gleam/dynamic/decode
5
import gleam/int
6
import gleam/json.{type Json}
···
9
import gleam/result
10
import honk/internal/constraints
11
import honk/internal/json_helpers
12
+
import honk/validation/context.{type ValidationContext}
13
14
const allowed_fields = [
15
"type", "minimum", "maximum", "enum", "const", "default", "description",
···
19
pub fn validate_schema(
20
schema: Json,
21
ctx: ValidationContext,
22
+
) -> Result(Nil, errors.ValidationError) {
23
let def_name = context.path(ctx)
24
25
// Validate allowed fields
···
75
data: Json,
76
schema: Json,
77
ctx: ValidationContext,
78
+
) -> Result(Nil, errors.ValidationError) {
79
let def_name = context.path(ctx)
80
81
// Check data is an integer
···
141
value: Int,
142
enum_values: List(Int),
143
def_name: String,
144
+
) -> Result(Nil, errors.ValidationError) {
145
constraints.validate_enum_constraint(
146
def_name,
147
value,
+4
-4
src/validation/primitive/null.gleam
src/honk/validation/primitive/null.gleam
+4
-4
src/validation/primitive/null.gleam
src/honk/validation/primitive/null.gleam
···
1
// Null type validator
2
3
-
import errors.{type ValidationError}
4
import gleam/json.{type Json}
5
import honk/internal/constraints
6
import honk/internal/json_helpers
7
-
import validation/context.{type ValidationContext}
8
9
const allowed_fields = ["type", "description"]
10
···
12
pub fn validate_schema(
13
schema: Json,
14
ctx: ValidationContext,
15
-
) -> Result(Nil, ValidationError) {
16
let def_name = context.path(ctx)
17
18
// Validate allowed fields
···
25
data: Json,
26
_schema: Json,
27
ctx: ValidationContext,
28
-
) -> Result(Nil, ValidationError) {
29
let def_name = context.path(ctx)
30
31
// Check data is null
···
1
// Null type validator
2
3
+
import honk/errors as errors
4
import gleam/json.{type Json}
5
import honk/internal/constraints
6
import honk/internal/json_helpers
7
+
import honk/validation/context.{type ValidationContext}
8
9
const allowed_fields = ["type", "description"]
10
···
12
pub fn validate_schema(
13
schema: Json,
14
ctx: ValidationContext,
15
+
) -> Result(Nil, errors.ValidationError) {
16
let def_name = context.path(ctx)
17
18
// Validate allowed fields
···
25
data: Json,
26
_schema: Json,
27
ctx: ValidationContext,
28
+
) -> Result(Nil, errors.ValidationError) {
29
let def_name = context.path(ctx)
30
31
// Check data is null
+10
-10
src/validation/primitive/string.gleam
src/honk/validation/primitive/string.gleam
+10
-10
src/validation/primitive/string.gleam
src/honk/validation/primitive/string.gleam
···
1
// String type validator
2
3
-
import errors.{type ValidationError}
4
import gleam/dynamic/decode
5
import gleam/json.{type Json}
6
import gleam/list
···
9
import gleam/string
10
import honk/internal/constraints
11
import honk/internal/json_helpers
12
-
import types
13
-
import validation/context.{type ValidationContext}
14
-
import validation/formats
15
16
const allowed_fields = [
17
"type", "format", "minLength", "maxLength", "minGraphemes", "maxGraphemes",
···
22
pub fn validate_schema(
23
schema: Json,
24
ctx: ValidationContext,
25
-
) -> Result(Nil, ValidationError) {
26
let def_name = context.path(ctx)
27
28
// Validate allowed fields
···
159
data: Json,
160
schema: Json,
161
ctx: ValidationContext,
162
-
) -> Result(Nil, ValidationError) {
163
let def_name = context.path(ctx)
164
165
// Check data is a string
···
233
min_length: Option(Int),
234
max_length: Option(Int),
235
def_name: String,
236
-
) -> Result(Nil, ValidationError) {
237
let byte_length = string.byte_size(value)
238
constraints.validate_length_constraints(
239
def_name,
···
250
min_graphemes: Option(Int),
251
max_graphemes: Option(Int),
252
def_name: String,
253
-
) -> Result(Nil, ValidationError) {
254
// Count grapheme clusters (visual characters) using Gleam's stdlib
255
// This correctly handles Unicode combining characters, emoji, etc.
256
let grapheme_count = value |> string.to_graphemes() |> list.length()
···
268
value: String,
269
format: types.StringFormat,
270
def_name: String,
271
-
) -> Result(Nil, ValidationError) {
272
case formats.validate_format(value, format) {
273
True -> Ok(Nil)
274
False -> {
···
285
value: String,
286
enum_values: List(String),
287
def_name: String,
288
-
) -> Result(Nil, ValidationError) {
289
constraints.validate_enum_constraint(
290
def_name,
291
value,
···
1
// String type validator
2
3
+
import honk/errors as errors
4
import gleam/dynamic/decode
5
import gleam/json.{type Json}
6
import gleam/list
···
9
import gleam/string
10
import honk/internal/constraints
11
import honk/internal/json_helpers
12
+
import honk/types as types
13
+
import honk/validation/context.{type ValidationContext}
14
+
import honk/validation/formats
15
16
const allowed_fields = [
17
"type", "format", "minLength", "maxLength", "minGraphemes", "maxGraphemes",
···
22
pub fn validate_schema(
23
schema: Json,
24
ctx: ValidationContext,
25
+
) -> Result(Nil, errors.ValidationError) {
26
let def_name = context.path(ctx)
27
28
// Validate allowed fields
···
159
data: Json,
160
schema: Json,
161
ctx: ValidationContext,
162
+
) -> Result(Nil, errors.ValidationError) {
163
let def_name = context.path(ctx)
164
165
// Check data is a string
···
233
min_length: Option(Int),
234
max_length: Option(Int),
235
def_name: String,
236
+
) -> Result(Nil, errors.ValidationError) {
237
let byte_length = string.byte_size(value)
238
constraints.validate_length_constraints(
239
def_name,
···
250
min_graphemes: Option(Int),
251
max_graphemes: Option(Int),
252
def_name: String,
253
+
) -> Result(Nil, errors.ValidationError) {
254
// Count grapheme clusters (visual characters) using Gleam's stdlib
255
// This correctly handles Unicode combining characters, emoji, etc.
256
let grapheme_count = value |> string.to_graphemes() |> list.length()
···
268
value: String,
269
format: types.StringFormat,
270
def_name: String,
271
+
) -> Result(Nil, errors.ValidationError) {
272
case formats.validate_format(value, format) {
273
True -> Ok(Nil)
274
False -> {
···
285
value: String,
286
enum_values: List(String),
287
def_name: String,
288
+
) -> Result(Nil, errors.ValidationError) {
289
constraints.validate_enum_constraint(
290
def_name,
291
value,
+2
-2
test/array_validator_test.gleam
+2
-2
test/array_validator_test.gleam
+2
-2
test/blob_validator_test.gleam
+2
-2
test/blob_validator_test.gleam
+2
-2
test/bytes_validator_test.gleam
+2
-2
test/bytes_validator_test.gleam
+4
-3
test/end_to_end_test.gleam
+4
-3
test/end_to_end_test.gleam
···
2
import gleeunit
3
import gleeunit/should
4
import honk
5
6
pub fn main() {
7
gleeunit.main()
···
192
193
// Test string format validation helper
194
pub fn validate_string_format_test() {
195
-
honk.validate_string_format("2024-01-01T12:00:00Z", honk.DateTime)
196
|> should.be_ok
197
198
-
honk.validate_string_format("not a datetime", honk.DateTime)
199
|> should.be_error
200
201
-
honk.validate_string_format("https://example.com", honk.Uri)
202
|> should.be_ok
203
}
···
2
import gleeunit
3
import gleeunit/should
4
import honk
5
+
import honk/types.{DateTime, Uri}
6
7
pub fn main() {
8
gleeunit.main()
···
193
194
// Test string format validation helper
195
pub fn validate_string_format_test() {
196
+
honk.validate_string_format("2024-01-01T12:00:00Z", DateTime)
197
|> should.be_ok
198
199
+
honk.validate_string_format("not a datetime", DateTime)
200
|> should.be_error
201
202
+
honk.validate_string_format("https://example.com", Uri)
203
|> should.be_ok
204
}
+1
-1
test/format_validator_test.gleam
+1
-1
test/format_validator_test.gleam
+2
-2
test/integer_validator_test.gleam
+2
-2
test/integer_validator_test.gleam
+2
-2
test/integration_test.gleam
+2
-2
test/integration_test.gleam
+2
-2
test/object_validator_test.gleam
+2
-2
test/object_validator_test.gleam
+2
-2
test/params_validator_test.gleam
+2
-2
test/params_validator_test.gleam
+2
-2
test/procedure_data_validation_test.gleam
+2
-2
test/procedure_data_validation_test.gleam
+2
-2
test/query_data_validation_test.gleam
+2
-2
test/query_data_validation_test.gleam
+3
-3
test/reference_validator_test.gleam
+3
-3
test/reference_validator_test.gleam
+2
-2
test/string_validator_test.gleam
+2
-2
test/string_validator_test.gleam
+2
-2
test/subscription_data_validation_test.gleam
+2
-2
test/subscription_data_validation_test.gleam
+2
-2
test/token_validator_test.gleam
+2
-2
test/token_validator_test.gleam
+2
-2
test/union_validator_test.gleam
+2
-2
test/union_validator_test.gleam
+2
-2
test/unknown_validator_test.gleam
+2
-2
test/unknown_validator_test.gleam