Source file json_schema_project.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
module Json = Chatoyant_runtime.Json
module Ast = Json_schema_ast

type warning = { path : string; message : string }
type result = { schema : Ast.t; warnings : warning list }

let identity schema = { schema; warnings = [] }
let has_field name fields = List.mem_assoc name fields

let replace_field name value fields =
  List.map
    (fun (field_name, field_value) ->
      if field_name = name then (field_name, value)
      else (field_name, field_value))
    fields

let property_names fields =
  match List.assoc_opt "properties" fields with
  | Some (Json.Object properties) -> List.map fst properties
  | _ -> []

let required_names fields =
  match List.assoc_opt "required" fields with
  | Some (Json.Array values) ->
      List.filter_map
        (function Json.String name -> Some name | _ -> None)
        values
  | _ -> []

let add_required_properties fields =
  let properties = property_names fields in
  if properties = [] then fields
  else
    let existing = required_names fields in
    let merged =
      existing
      @ List.filter (fun name -> not (List.mem name existing)) properties
    in
    let required =
      Json.Array (List.map (fun name -> Json.String name) merged)
    in
    if has_field "required" fields then replace_field "required" required fields
    else fields @ [ ("required", required) ]

let rec project_json path json =
  match json with
  | Json.Object fields ->
      let fields =
        fields
        |> List.map (fun (name, value) -> (name, project_child path name value))
      in
      let fields =
        if
          has_field "properties" fields
          && not (has_field "additionalProperties" fields)
        then fields @ [ ("additionalProperties", Json.Bool false) ]
        else fields
      in
      let fields =
        if has_field "properties" fields then add_required_properties fields
        else fields
      in
      Json.Object fields
  | Json.Array values ->
      Json.Array
        (List.mapi
           (fun index -> project_json (path ^ "/" ^ string_of_int index))
           values)
  | value -> value

and project_child path name value =
  match name with
  | "$defs" | "definitions" | "properties" | "patternProperties"
  | "dependentSchemas" -> (
      match value with
      | Json.Object fields ->
          Json.Object
            (List.map
               (fun (name, schema) ->
                 (name, project_json (path ^ "/" ^ name) schema))
               fields)
      | _ -> value)
  | "prefixItems" | "allOf" | "anyOf" | "oneOf" -> (
      match value with
      | Json.Array values ->
          Json.Array
            (List.mapi
               (fun index schema ->
                 project_json
                   (path ^ "/" ^ name ^ "/" ^ string_of_int index)
                   schema)
               values)
      | _ -> value)
  | "items" | "contains" | "additionalProperties" | "unevaluatedProperties"
  | "unevaluatedItems" | "propertyNames" | "not" | "if" | "then" | "else" ->
      project_json (path ^ "/" ^ name) value
  | _ -> value

let openai_strict schema =
  let projected = schema |> Ast.to_json |> project_json "" in
  match Ast.of_json projected with
  | Ok schema -> { schema; warnings = [] }
  | Error error ->
      {
        schema;
        warnings =
          [
            {
              path = error.path;
              message = "projection produced invalid schema: " ^ error.message;
            };
          ];
      }