Source file json_schema_resolver.ml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
module Json = Chatoyant_runtime.Json
module Pointer = Json_schema_pointer
type target = { uri : string; base_uri : string; schema : Json.t }
type resource = { uri : string; schema : Json.t }
type t = {
root : Json.t;
root_base_uri : string;
resources : (string * Json.t) list;
anchors : (string * Json.t) list;
dynamic_anchors : (string * Json.t) list;
}
let root registry = registry.root
let root_base_uri registry = registry.root_base_uri
let has_scheme uri =
match String.index_opt uri ':' with
| None -> false
| Some index ->
index > 0
&&
let rec loop i =
if i >= index then true
else
match uri.[i] with
| 'a' .. 'z' | 'A' .. 'Z' | '0' .. '9' | '+' | '-' | '.' ->
loop (i + 1)
| _ -> false
in
loop 0
let split_document uri = fst (Pointer.split_uri_fragment uri)
let normalize_path path =
let absolute = String.length path > 0 && path.[0] = '/' in
let trailing_slash =
String.length path > 1 && path.[String.length path - 1] = '/'
in
let parts =
path |> String.split_on_char '/'
|> List.fold_left
(fun acc part ->
match part with
| "" | "." -> acc
| ".." -> ( match acc with [] -> [] | _ :: rest -> rest)
| value -> value :: acc)
[]
|> List.rev
in
let normalized = (if absolute then "/" else "") ^ String.concat "/" parts in
if trailing_slash && normalized <> "/" then normalized ^ "/" else normalized
let scheme_authority uri =
match String.index_opt uri ':' with
| None -> None
| Some colon ->
if
colon + 2 < String.length uri
&& uri.[colon + 1] = '/'
&& uri.[colon + 2] = '/'
then
let start = colon + 3 in
let stop =
match String.index_from_opt uri start '/' with
| Some index -> index
| None -> String.length uri
in
Some
( String.sub uri 0 colon,
String.sub uri start (stop - start),
if stop < String.length uri then
String.sub uri stop (String.length uri - stop)
else "/" )
else None
let directory document =
match String.rindex_opt document '/' with
| None -> ""
| Some index -> String.sub document 0 (index + 1)
let resolve_uri ~base_uri reference =
if reference = "" then split_document base_uri
else if has_scheme reference then reference
else
let base_document = split_document base_uri in
if String.length reference > 0 && reference.[0] = '#' then
base_document ^ reference
else if String.length reference > 0 && reference.[0] = '/' then
match scheme_authority base_document with
| Some (scheme, authority, _) ->
scheme ^ "://" ^ authority ^ normalize_path reference
| None -> normalize_path reference
else
match scheme_authority base_document with
| Some (scheme, authority, path) ->
let combined = normalize_path (directory path ^ reference) in
scheme ^ "://" ^ authority ^ combined
| None -> normalize_path (directory base_document ^ reference)
let without_fragment uri = split_document uri
let string_ends_with value suffix =
let value_len = String.length value in
let suffix_len = String.length suffix in
suffix_len <= value_len
&& String.sub value (value_len - suffix_len) suffix_len = suffix
let json_string_field name = function
| Json.Object fields ->
Option.bind (List.assoc_opt name fields) Json.as_string
| _ -> None
let is_schema_keyword name =
match name with
| "additionalProperties" | "unevaluatedProperties" | "propertyNames" | "items"
| "contains" | "unevaluatedItems" | "not" | "if" | "then" | "else" ->
true
| _ -> false
let is_schema_array_keyword name =
match name with
| "prefixItems" | "allOf" | "anyOf" | "oneOf" -> true
| _ -> false
let object_values = function
| Json.Object fields -> List.map snd fields
| _ -> []
let array_values = function Json.Array values -> values | _ -> []
let add_unique key value entries =
if List.mem_assoc key entries then entries else (key, value) :: entries
let merge_unique entries additions =
List.fold_left
(fun acc (key, value) -> add_unique key value acc)
entries additions
let scan root_base root =
let rec loop base schema (resources, anchors, dynamic_anchors) =
match schema with
| Json.Bool _ -> (resources, anchors, dynamic_anchors)
| Json.Object fields ->
let base =
match json_string_field "$id" schema with
| None -> base
| Some id -> resolve_uri ~base_uri:base id |> without_fragment
in
let resources = add_unique base schema resources in
let anchors =
match json_string_field "$anchor" schema with
| None -> anchors
| Some anchor -> add_unique (base ^ "#" ^ anchor) schema anchors
in
let dynamic_anchors =
match json_string_field "$dynamicAnchor" schema with
| None -> dynamic_anchors
| Some anchor ->
add_unique (base ^ "#" ^ anchor) schema dynamic_anchors
in
List.fold_left
(fun acc (name, value) ->
if is_schema_keyword name then
match value with
| Json.Bool _ | Json.Object _ -> loop base value acc
| Json.Array _ | Json.Float _ | Json.String _ | Json.Null -> acc
else if
name = "$defs" || name = "definitions" || name = "properties"
|| name = "patternProperties" || name = "dependentSchemas"
then
List.fold_left
(fun acc value -> loop base value acc)
acc (object_values value)
else if is_schema_array_keyword name then
List.fold_left
(fun acc value -> loop base value acc)
acc (array_values value)
else acc)
(resources, anchors, dynamic_anchors)
fields
| _ -> (resources, anchors, dynamic_anchors)
in
loop root_base root ([], [], [])
let scan_resource ~retrieval_uri schema (resources, anchors, dynamic_anchors) =
let base_uri =
match json_string_field "$id" schema with
| None -> without_fragment retrieval_uri
| Some id -> resolve_uri ~base_uri:retrieval_uri id |> without_fragment
in
let scanned_resources, scanned_anchors, scanned_dynamic_anchors =
scan base_uri schema
in
let resources = merge_unique resources scanned_resources in
let anchors = merge_unique anchors scanned_anchors in
let dynamic_anchors = merge_unique dynamic_anchors scanned_dynamic_anchors in
let resources =
add_unique (without_fragment retrieval_uri) schema resources
in
(resources, anchors, dynamic_anchors, base_uri)
let create ?(base_uri = "") ?(resources = []) root =
let root_resources, root_anchors, root_dynamic_anchors, root_base_uri =
scan_resource ~retrieval_uri:base_uri root ([], [], [])
in
let resources, anchors, dynamic_anchors =
List.fold_left
(fun (resources, anchors, dynamic_anchors) resource ->
let scanned_resources, scanned_anchors, scanned_dynamic_anchors, _ =
scan_resource ~retrieval_uri:resource.uri resource.schema
(resources, anchors, dynamic_anchors)
in
(scanned_resources, scanned_anchors, scanned_dynamic_anchors))
(root_resources, root_anchors, root_dynamic_anchors)
resources
in
let resources =
add_unique "https://json-schema.org/draft/2020-12/schema" (Json.Bool true)
resources
in
{
root;
root_base_uri;
resources = List.rev resources;
anchors = List.rev anchors;
dynamic_anchors = List.rev dynamic_anchors;
}
let child_base_uri _registry ~base_uri schema =
match json_string_field "$id" schema with
| None -> base_uri
| Some id ->
let resolved = resolve_uri ~base_uri id |> without_fragment in
let id_document = without_fragment id in
if
base_uri <> ""
&& (base_uri = resolved
|| (not (has_scheme id_document))
&& string_ends_with base_uri id_document)
then base_uri
else resolved
let resource registry document =
if document = "" then Some registry.root
else List.assoc_opt document registry.resources
let resolve registry ~base_uri reference =
let absolute = resolve_uri ~base_uri reference in
let document, fragment = Pointer.split_uri_fragment absolute in
let document = if document = "" then split_document base_uri else document in
match Pointer.parse_fragment fragment with
| Pointer.Empty -> (
match resource registry document with
| None -> None
| Some schema -> Some { uri = document; base_uri = document; schema })
| Pointer.Pointer tokens -> (
match resource registry document with
| None -> None
| Some schema -> (
match Pointer.resolve_pointer schema tokens with
| None -> None
| Some schema ->
Some
{
uri = document ^ "#" ^ Pointer.pointer_to_string tokens;
base_uri = document;
schema;
}))
| Pointer.Anchor anchor -> (
match List.assoc_opt (document ^ "#" ^ anchor) registry.anchors with
| Some schema ->
Some { uri = document ^ "#" ^ anchor; base_uri = document; schema }
| None -> (
match
List.assoc_opt (document ^ "#" ^ anchor) registry.dynamic_anchors
with
| Some schema ->
Some
{ uri = document ^ "#" ^ anchor; base_uri = document; schema }
| None -> None))
let dynamic_anchors_for_resource registry base_uri =
let document = without_fragment base_uri in
registry.dynamic_anchors
|> List.filter_map (fun (uri, schema) ->
let document', fragment = Pointer.split_uri_fragment uri in
match Pointer.parse_fragment fragment with
| Pointer.Anchor anchor when document' = document ->
Some (anchor, { uri; base_uri = document; schema })
| _ -> None)
let resolve_dynamic registry ~base_uri ~dynamic_scope reference =
let absolute = resolve_uri ~base_uri reference in
let _, fragment = Pointer.split_uri_fragment absolute in
match Pointer.parse_fragment fragment with
| Pointer.Anchor anchor -> (
let static = resolve registry ~base_uri reference in
let static_is_dynamic =
match static with
| None -> false
| Some target -> (
match json_string_field "$dynamicAnchor" target.schema with
| Some dynamic_anchor when dynamic_anchor = anchor -> true
| _ -> false)
in
if not static_is_dynamic then static
else
match List.assoc_opt anchor dynamic_scope with
| Some target -> Some target
| None -> resolve registry ~base_uri reference)
| _ -> resolve registry ~base_uri reference