An ATProto Lexicon validator for Gleam.
1import gleam/json
2import gleeunit
3import gleeunit/should
4import honk/validation/context
5import honk/validation/field
6import honk/validation/field/reference
7
8pub fn main() {
9 gleeunit.main()
10}
11
12// ========== SCHEMA VALIDATION TESTS ==========
13
14pub fn valid_local_reference_schema_test() {
15 let schema =
16 json.object([#("type", json.string("ref")), #("ref", json.string("#post"))])
17
18 let assert Ok(ctx) = context.builder() |> context.build
19
20 reference.validate_schema(schema, ctx) |> should.be_ok
21}
22
23pub fn valid_global_reference_schema_test() {
24 let schema =
25 json.object([
26 #("type", json.string("ref")),
27 #("ref", json.string("com.atproto.repo.strongRef#main")),
28 ])
29
30 let assert Ok(ctx) = context.builder() |> context.build
31
32 reference.validate_schema(schema, ctx) |> should.be_ok
33}
34
35pub fn valid_global_main_reference_schema_test() {
36 let schema =
37 json.object([
38 #("type", json.string("ref")),
39 #("ref", json.string("com.atproto.repo.strongRef")),
40 ])
41
42 let assert Ok(ctx) = context.builder() |> context.build
43
44 reference.validate_schema(schema, ctx) |> should.be_ok
45}
46
47pub fn invalid_empty_ref_test() {
48 let schema =
49 json.object([#("type", json.string("ref")), #("ref", json.string(""))])
50
51 let assert Ok(ctx) = context.builder() |> context.build
52
53 reference.validate_schema(schema, ctx) |> should.be_error
54}
55
56pub fn invalid_missing_ref_field_test() {
57 let schema = json.object([#("type", json.string("ref"))])
58
59 let assert Ok(ctx) = context.builder() |> context.build
60
61 reference.validate_schema(schema, ctx) |> should.be_error
62}
63
64pub fn invalid_local_ref_no_def_name_test() {
65 let schema =
66 json.object([#("type", json.string("ref")), #("ref", json.string("#"))])
67
68 let assert Ok(ctx) = context.builder() |> context.build
69
70 reference.validate_schema(schema, ctx) |> should.be_error
71}
72
73pub fn invalid_global_ref_empty_nsid_test() {
74 // Test that a global reference must have an NSID before the #
75 // The reference "com.example#main" is valid, but starting with just # makes it local
76 // This test actually verifies that "#" alone (empty def name) is invalid
77 let schema =
78 json.object([#("type", json.string("ref")), #("ref", json.string("#"))])
79
80 let assert Ok(ctx) = context.builder() |> context.build
81
82 reference.validate_schema(schema, ctx) |> should.be_error
83}
84
85pub fn invalid_global_ref_empty_def_test() {
86 let schema =
87 json.object([
88 #("type", json.string("ref")),
89 #("ref", json.string("com.example.lexicon#")),
90 ])
91
92 let assert Ok(ctx) = context.builder() |> context.build
93
94 reference.validate_schema(schema, ctx) |> should.be_error
95}
96
97pub fn invalid_multiple_hash_test() {
98 let schema =
99 json.object([
100 #("type", json.string("ref")),
101 #("ref", json.string("com.example#foo#bar")),
102 ])
103
104 let assert Ok(ctx) = context.builder() |> context.build
105
106 reference.validate_schema(schema, ctx) |> should.be_error
107}
108
109// ========== DATA VALIDATION TESTS ==========
110
111pub fn valid_reference_to_string_test() {
112 // Create a simple lexicon with a string definition
113 let defs =
114 json.object([
115 #(
116 "post",
117 json.object([
118 #("type", json.string("string")),
119 #("maxLength", json.int(280)),
120 ]),
121 ),
122 ])
123
124 let lexicon =
125 json.object([
126 #("lexicon", json.int(1)),
127 #("id", json.string("app.bsky.feed.post")),
128 #("defs", defs),
129 ])
130
131 let assert Ok(builder) =
132 context.builder()
133 |> context.with_validator(field.dispatch_data_validation)
134 |> context.with_lexicons([lexicon])
135
136 let assert Ok(ctx) = context.build(builder)
137 let ctx = context.with_current_lexicon(ctx, "app.bsky.feed.post")
138
139 let ref_schema =
140 json.object([#("type", json.string("ref")), #("ref", json.string("#post"))])
141
142 let data = json.string("Hello, world!")
143
144 reference.validate_data(data, ref_schema, ctx)
145 |> should.be_ok
146}
147
148pub fn valid_reference_to_object_test() {
149 // Create a lexicon with an object definition
150 let defs =
151 json.object([
152 #(
153 "user",
154 json.object([
155 #("type", json.string("object")),
156 #(
157 "properties",
158 json.object([
159 #(
160 "name",
161 json.object([
162 #("type", json.string("string")),
163 #("required", json.bool(True)),
164 ]),
165 ),
166 ]),
167 ),
168 ]),
169 ),
170 ])
171
172 let lexicon =
173 json.object([
174 #("lexicon", json.int(1)),
175 #("id", json.string("app.test.schema")),
176 #("defs", defs),
177 ])
178
179 let assert Ok(builder) =
180 context.builder()
181 |> context.with_validator(field.dispatch_data_validation)
182 |> context.with_lexicons([lexicon])
183
184 let assert Ok(ctx) = context.build(builder)
185 let ctx = context.with_current_lexicon(ctx, "app.test.schema")
186
187 let ref_schema =
188 json.object([#("type", json.string("ref")), #("ref", json.string("#user"))])
189
190 let data = json.object([#("name", json.string("Alice"))])
191
192 reference.validate_data(data, ref_schema, ctx)
193 |> should.be_ok
194}
195
196pub fn invalid_reference_not_found_test() {
197 let defs = json.object([])
198
199 let lexicon =
200 json.object([
201 #("lexicon", json.int(1)),
202 #("id", json.string("app.test.schema")),
203 #("defs", defs),
204 ])
205
206 let assert Ok(builder) =
207 context.builder()
208 |> context.with_validator(field.dispatch_data_validation)
209 |> context.with_lexicons([lexicon])
210
211 let assert Ok(ctx) = context.build(builder)
212 let ctx = context.with_current_lexicon(ctx, "app.test.schema")
213
214 let ref_schema =
215 json.object([
216 #("type", json.string("ref")),
217 #("ref", json.string("#nonexistent")),
218 ])
219
220 let data = json.string("test")
221
222 reference.validate_data(data, ref_schema, ctx)
223 |> should.be_error
224}
225
226pub fn circular_reference_detection_test() {
227 // Create lexicon with circular reference: A -> B -> A
228 let defs =
229 json.object([
230 #(
231 "refA",
232 json.object([
233 #("type", json.string("ref")),
234 #("ref", json.string("#refB")),
235 ]),
236 ),
237 #(
238 "refB",
239 json.object([
240 #("type", json.string("ref")),
241 #("ref", json.string("#refA")),
242 ]),
243 ),
244 ])
245
246 let lexicon =
247 json.object([
248 #("lexicon", json.int(1)),
249 #("id", json.string("app.test.circular")),
250 #("defs", defs),
251 ])
252
253 let assert Ok(builder) =
254 context.builder()
255 |> context.with_validator(field.dispatch_data_validation)
256 |> context.with_lexicons([lexicon])
257
258 let assert Ok(ctx) = context.build(builder)
259 let ctx = context.with_current_lexicon(ctx, "app.test.circular")
260
261 let ref_schema =
262 json.object([#("type", json.string("ref")), #("ref", json.string("#refA"))])
263
264 let data = json.string("test")
265
266 // Should detect the circular reference and return an error
267 reference.validate_data(data, ref_schema, ctx)
268 |> should.be_error
269}
270
271pub fn nested_reference_chain_test() {
272 // Create lexicon with nested references: A -> B -> string
273 let defs =
274 json.object([
275 #(
276 "refA",
277 json.object([
278 #("type", json.string("ref")),
279 #("ref", json.string("#refB")),
280 ]),
281 ),
282 #(
283 "refB",
284 json.object([
285 #("type", json.string("ref")),
286 #("ref", json.string("#actualString")),
287 ]),
288 ),
289 #("actualString", json.object([#("type", json.string("string"))])),
290 ])
291
292 let lexicon =
293 json.object([
294 #("lexicon", json.int(1)),
295 #("id", json.string("app.test.nested")),
296 #("defs", defs),
297 ])
298
299 let assert Ok(builder) =
300 context.builder()
301 |> context.with_validator(field.dispatch_data_validation)
302 |> context.with_lexicons([lexicon])
303
304 let assert Ok(ctx) = context.build(builder)
305 let ctx = context.with_current_lexicon(ctx, "app.test.nested")
306
307 let ref_schema =
308 json.object([#("type", json.string("ref")), #("ref", json.string("#refA"))])
309
310 let data = json.string("Hello!")
311
312 reference.validate_data(data, ref_schema, ctx)
313 |> should.be_ok
314}
315
316pub fn cross_lexicon_reference_test() {
317 // Create two lexicons where one references the other
318 let lex1_defs =
319 json.object([
320 #(
321 "userRef",
322 json.object([
323 #("type", json.string("ref")),
324 #("ref", json.string("app.test.types#user")),
325 ]),
326 ),
327 ])
328
329 let lex2_defs =
330 json.object([
331 #(
332 "user",
333 json.object([
334 #("type", json.string("object")),
335 #(
336 "properties",
337 json.object([
338 #(
339 "id",
340 json.object([
341 #("type", json.string("string")),
342 #("required", json.bool(True)),
343 ]),
344 ),
345 ]),
346 ),
347 ]),
348 ),
349 ])
350
351 let lex1 =
352 json.object([
353 #("lexicon", json.int(1)),
354 #("id", json.string("app.test.schema")),
355 #("defs", lex1_defs),
356 ])
357
358 let lex2 =
359 json.object([
360 #("lexicon", json.int(1)),
361 #("id", json.string("app.test.types")),
362 #("defs", lex2_defs),
363 ])
364
365 let assert Ok(builder) =
366 context.builder()
367 |> context.with_validator(field.dispatch_data_validation)
368 |> context.with_lexicons([lex1, lex2])
369
370 let assert Ok(ctx) = context.build(builder)
371 let ctx = context.with_current_lexicon(ctx, "app.test.schema")
372
373 let ref_schema =
374 json.object([
375 #("type", json.string("ref")),
376 #("ref", json.string("#userRef")),
377 ])
378
379 let data = json.object([#("id", json.string("user123"))])
380
381 reference.validate_data(data, ref_schema, ctx)
382 |> should.be_ok
383}