An ATProto Lexicon validator for Gleam.

reorg pub exports, add example with goose

+1 -1
.gitignore
··· 1 *.beam 2 *.ez 3 - /build 4 erl_crash.dump 5 .claude
··· 1 *.beam 2 *.ez 3 + build/ 4 erl_crash.dump 5 .claude
+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
···
··· 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
···
··· 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
···
··· 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
···
··· 1 + import gleeunit 2 + 3 + pub fn main() -> Nil { 4 + gleeunit.main() 5 + } 6 + 7 + // gleeunit test functions end in `_test` 8 + pub fn hello_world_test() { 9 + let name = "Joe" 10 + let greeting = "Hello, " <> name <> "!" 11 + 12 + assert greeting == "Hello, Joe!" 13 + }
src/errors.gleam src/honk/errors.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
··· 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
··· 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
··· 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
+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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 1 import gleam/json 2 import gleeunit 3 import gleeunit/should 4 - import validation/context 5 - import validation/field 6 7 pub fn main() { 8 gleeunit.main()
··· 1 import gleam/json 2 import gleeunit 3 import gleeunit/should 4 + import honk/validation/context 5 + import honk/validation/field 6 7 pub fn main() { 8 gleeunit.main()
+2 -2
test/blob_validator_test.gleam
··· 1 import gleam/json 2 import gleeunit 3 import gleeunit/should 4 - import validation/context 5 - import validation/primitive/blob 6 7 pub fn main() { 8 gleeunit.main()
··· 1 import gleam/json 2 import gleeunit 3 import gleeunit/should 4 + import honk/validation/context 5 + import honk/validation/primitive/blob 6 7 pub fn main() { 8 gleeunit.main()
+2 -2
test/bytes_validator_test.gleam
··· 1 import gleam/json 2 import gleeunit 3 import gleeunit/should 4 - import validation/context 5 - import validation/primitive/bytes 6 7 pub fn main() { 8 gleeunit.main()
··· 1 import gleam/json 2 import gleeunit 3 import gleeunit/should 4 + import honk/validation/context 5 + import honk/validation/primitive/bytes 6 7 pub fn main() { 8 gleeunit.main()
+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 import gleeunit 2 import gleeunit/should 3 - import validation/formats 4 5 pub fn main() { 6 gleeunit.main()
··· 1 import gleeunit 2 import gleeunit/should 3 + import honk/validation/formats 4 5 pub fn main() { 6 gleeunit.main()
+2 -2
test/integer_validator_test.gleam
··· 1 import gleam/json 2 import gleeunit 3 import gleeunit/should 4 - import validation/context 5 - import validation/primitive/integer 6 7 pub fn main() { 8 gleeunit.main()
··· 1 import gleam/json 2 import gleeunit 3 import gleeunit/should 4 + import honk/validation/context 5 + import honk/validation/primitive/integer 6 7 pub fn main() { 8 gleeunit.main()
+2 -2
test/integration_test.gleam
··· 1 import gleam/json 2 import gleeunit 3 import gleeunit/should 4 - import validation/context 5 - import validation/primary/record 6 7 pub fn main() { 8 gleeunit.main()
··· 1 import gleam/json 2 import gleeunit 3 import gleeunit/should 4 + import honk/validation/context 5 + import honk/validation/primary/record 6 7 pub fn main() { 8 gleeunit.main()
+2 -2
test/object_validator_test.gleam
··· 1 import gleam/json 2 import gleeunit 3 import gleeunit/should 4 - import validation/context 5 - import validation/field 6 7 pub fn main() { 8 gleeunit.main()
··· 1 import gleam/json 2 import gleeunit 3 import gleeunit/should 4 + import honk/validation/context 5 + import honk/validation/field 6 7 pub fn main() { 8 gleeunit.main()
+2 -2
test/params_validator_test.gleam
··· 1 import gleam/json 2 import gleeunit 3 import gleeunit/should 4 - import validation/context 5 - import validation/primary/params 6 7 pub fn main() { 8 gleeunit.main()
··· 1 import gleam/json 2 import gleeunit 3 import gleeunit/should 4 + import honk/validation/context 5 + import honk/validation/primary/params 6 7 pub fn main() { 8 gleeunit.main()
+2 -2
test/procedure_data_validation_test.gleam
··· 1 import gleam/json 2 import gleeunit 3 import gleeunit/should 4 - import validation/context 5 - import validation/primary/procedure 6 7 pub fn main() { 8 gleeunit.main()
··· 1 import gleam/json 2 import gleeunit 3 import gleeunit/should 4 + import honk/validation/context 5 + import honk/validation/primary/procedure 6 7 pub fn main() { 8 gleeunit.main()
+2 -2
test/query_data_validation_test.gleam
··· 1 import gleam/json 2 import gleeunit 3 import gleeunit/should 4 - import validation/context 5 - import validation/primary/query 6 7 pub fn main() { 8 gleeunit.main()
··· 1 import gleam/json 2 import gleeunit 3 import gleeunit/should 4 + import honk/validation/context 5 + import honk/validation/primary/query 6 7 pub fn main() { 8 gleeunit.main()
+3 -3
test/reference_validator_test.gleam
··· 1 import gleam/json 2 import gleeunit 3 import gleeunit/should 4 - import validation/context 5 - import validation/field 6 - import validation/field/reference 7 8 pub fn main() { 9 gleeunit.main()
··· 1 import gleam/json 2 import gleeunit 3 import gleeunit/should 4 + import honk/validation/context 5 + import honk/validation/field 6 + import honk/validation/field/reference 7 8 pub fn main() { 9 gleeunit.main()
+2 -2
test/string_validator_test.gleam
··· 1 import gleam/json 2 import gleeunit 3 import gleeunit/should 4 - import validation/context 5 - import validation/primitive/string 6 7 pub fn main() { 8 gleeunit.main()
··· 1 import gleam/json 2 import gleeunit 3 import gleeunit/should 4 + import honk/validation/context 5 + import honk/validation/primitive/string 6 7 pub fn main() { 8 gleeunit.main()
+2 -2
test/subscription_data_validation_test.gleam
··· 1 import gleam/json 2 import gleeunit 3 import gleeunit/should 4 - import validation/context 5 - import validation/primary/subscription 6 7 pub fn main() { 8 gleeunit.main()
··· 1 import gleam/json 2 import gleeunit 3 import gleeunit/should 4 + import honk/validation/context 5 + import honk/validation/primary/subscription 6 7 pub fn main() { 8 gleeunit.main()
+2 -2
test/token_validator_test.gleam
··· 1 import gleam/json 2 import gleeunit 3 import gleeunit/should 4 - import validation/context 5 - import validation/meta/token 6 7 pub fn main() { 8 gleeunit.main()
··· 1 import gleam/json 2 import gleeunit 3 import gleeunit/should 4 + import honk/validation/context 5 + import honk/validation/meta/token 6 7 pub fn main() { 8 gleeunit.main()
+2 -2
test/union_validator_test.gleam
··· 1 import gleam/json 2 import gleeunit 3 import gleeunit/should 4 - import validation/context 5 - import validation/field/union 6 7 pub fn main() { 8 gleeunit.main()
··· 1 import gleam/json 2 import gleeunit 3 import gleeunit/should 4 + import honk/validation/context 5 + import honk/validation/field/union 6 7 pub fn main() { 8 gleeunit.main()
+2 -2
test/unknown_validator_test.gleam
··· 1 import gleam/json 2 import gleeunit 3 import gleeunit/should 4 - import validation/context 5 - import validation/meta/unknown 6 7 pub fn main() { 8 gleeunit.main()
··· 1 import gleam/json 2 import gleeunit 3 import gleeunit/should 4 + import honk/validation/context 5 + import honk/validation/meta/unknown 6 7 pub fn main() { 8 gleeunit.main()