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
(* Copyright 2022-2023 Kotoi-Xie Consultancy, Inc. This file is a part of the

==== Bindoj (https://kxc.dev/bindoj) ====

software project that is developed, maintained, and distributed by
Kotoi-Xie Consultancy, Inc. (https://kxc.inc) which is also known as KXC.

Licensed under the Apache License, Version 2.0 (the "License"); you may not
use this file except in compliance with the License. You may obtain a copy
of the License at http://www.apache.org/licenses/LICENSE-2.0. Unless required
by applicable law or agreed to in writing, software distributed under the
License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS
OF ANY KIND, either express or implied. See the License for the specific
language governing permissions and limitations under the License.
                                                                              *)
(* Acknowledgements  --- AnchorZ Inc. ---  The current/initial version or a
significant portion of this file is developed under the funding provided by
AnchorZ Inc. to satisfy its needs in its product development workflow.
                                                                              *)
open Bindoj_typedesc.Typed_type_desc
open Bindoj_runtime
open Bindoj_gen.Json_codec
open Bindoj_apidir_shared
module OpenApi = Bindoj_openapi.V3
module Codec = Bindoj_codec

let content_type = "application/json" (* assert Content-Type is always application/json *)

let server_of_url url =
  let url = Uri.of_string url in
  match Uri.host url with
  | Some host ->
    let host_url = Uri.make ~scheme:(Uri.scheme url |? "https") ~host () in
    Some (Uri.to_string host_url)
  | None -> None

let path_of_url url =
  let url = Uri.of_string url in
  Uri.path_and_query url

let rec gen_openapi_document_object :
  title:string -> version:string -> registry_info -> OpenApi.Document_object.t =
  fun ~title ~version reg_info ->
  let components = openapi_components_object_of_type_decl_collection reg_info in
  let paths_object = openapi_paths_object_of_invocation_point_collection reg_info in
  let info_object = OpenApi.Info_object.mk title version in
  let openapi_version = "3.0.3" in
  OpenApi.Document_object.mk ~components openapi_version info_object paths_object

and openapi_components_object_of_type_decl_collection :
  registry_info -> OpenApi.Components_object.t =
  fun registry_info ->
  let { prim_ident_typemap; alias_ident_typemap; } : tdenv =
    tdenv_of_registry_info registry_info in
  let decls =
    (prim_ident_typemap
     |> StringMap.to_list |&> fun (_, Boxed_prim (_, typed)) -> Typed.decl typed) @
    (alias_ident_typemap
     |> StringMap.to_list |&> fun (_, boxed) -> (Typed.decl % Typed.unbox) boxed) in
  let decl_schemas =
    decls |> List.map (fun td ->
      fst (Json_config.get_mangled_name_of_type td),
      Either.left (openapi_schema_object_of_type_decl td))
    in
  let references =
    alias_ident_typemap
    |> StringMap.to_list
    |> List.filter_map (fun (name, boxed) ->
      let decl = (Typed.decl % Typed.unbox) boxed in
      if name = decl.td_name then None
      else
        let json_name = fst (Json_config.get_mangled_name_of_type decl) in
        let ref = OpenApi.Reference_object.mk (sprintf "#/components/schemas/%s" json_name) in
        Some (json_name, Either.right ref)) in
  let schemas =
    decl_schemas @ references
    |> List.sort_uniq (fun (n1, _) (n2, _) -> compare n1 n2) in
  OpenApi.Components_object.mk ~schemas ()

and openapi_paths_object_of_invocation_point_collection :
  registry_info -> OpenApi.Path_item_object.paths_object =
  fun reg_info ->
  OpenApi.Path_item_object.paths (
    reg_info
    |> fst
    |&> function
      | Invp invp ->
        let invp : ('reqty, 'respty) invocation_point_info = Obj.magic invp in
        let path_item = openapi_path_item_object_of_invocation_point_info reg_info invp in
        (path_of_url invp.ip_urlpath, path_item))

and openapi_path_item_object_of_invocation_point_info :
  registry_info -> ('reqty, 'respty) invocation_point_info -> OpenApi.Path_item_object.t =
  fun reg_info invp ->
  let { ip_name; ip_urlpath; ip_method;
        ip_request_body; ip_responses; ip_deprecated;
        ip_summary = summary; ip_description = description; ip_tags;
        ip_external_doc; ip_usage_samples } = invp in
  let summary = summary |? ip_name in
  let tags = match ip_tags with [] -> None | xs -> Some xs in
  let req_samples =
    ip_usage_samples
    |> List.filter_map (function
      | Req_sample s -> Some s
      | Resp_sample _ -> None
      | Usage_sample ((req, _, _), doc) -> Some (req,doc))
  in
  let resp_samples =
    ip_usage_samples
    |> List.filter_map (function
    | Req_sample _ -> None
    | Resp_sample s -> Some s
    | Usage_sample ((_, resp, status), doc) -> Some ((resp, status), doc))
  in
  let mk_path_item_object op =
    let servers =
      match server_of_url ip_urlpath with
      | Some server -> Some [OpenApi.Server_object.mk server]
      | None -> None
    in
    match ip_method with
    | `get -> OpenApi.Path_item_object.mk ~summary ?description ?servers ~get:op ()
    | `post -> OpenApi.Path_item_object.mk ~summary ?description ?servers ~post:op ()
  in
  let mk_operation_object request_body responses =
    let externalDocs = ip_external_doc |> Option.map openapi_external_documentation_object_of_external_doc in
    let requestBody = request_body |> Option.map Either.left in
    OpenApi.Path_item_object.operation
      ~deprecated:ip_deprecated
      ~summary ?description ?tags ?externalDocs ?requestBody
      responses
  in
  let request_body =
    (Obj.magic ip_request_body) |> Option.map (openapi_request_body_object_of_request_body reg_info (Obj.magic req_samples)) in
  let responses =
    ip_responses |&> function Response_case { status; response; samples; unpack; _ } ->
      let samples : 'a with_doc list =
        resp_samples
        |&?> (function
          | ((v, st), doc) when st = status ->
            unpack v >? (fun v -> (v, doc))
          | _ -> None)
        |> List.append samples
        |> Obj.magic
      in
      let response = openapi_response_object_of_response reg_info (samples) (Obj.magic response) in
      (status, Either.left response) in
  let operation = mk_operation_object request_body responses in
  mk_path_item_object operation

and openapi_request_body_object_of_request_body : registry_info -> 't with_doc list -> 't request_body -> OpenApi.Request_body_object.t =
  fun reg_info req_samples { rq_media_type; rq_description; rq_required; } ->
  let media_type = openapi_media_type_object_of_media_type reg_info req_samples rq_media_type in
  OpenApi.Request_body_object.mk
    ~description:rq_description
    ~required:rq_required
    [(content_type, media_type)]

and openapi_response_object_of_response : registry_info -> 't with_doc list -> 't response -> OpenApi.Response_object.t =
  fun reg_info resp_samples { rs_media_type; rs_description; rs_headers; } ->
  let media_type = openapi_media_type_object_of_media_type reg_info resp_samples rs_media_type in
  let headers =
    rs_headers |&> fun header ->
      let { hd_name; _; } = header in
      let header = openapi_header_object_of_header reg_info header in
      (hd_name, Either.left header) in
  match headers with
  | [] -> OpenApi.Response_object.mk
            ~content:[(content_type, media_type)]
            rs_description
  | _ -> OpenApi.Response_object.mk
           ~headers
           ~content:[(content_type, media_type)]
           rs_description

and openapi_header_object_of_header : registry_info -> 't header -> OpenApi.Header_object.t =
  fun reg_info { hd_name=_; hd_description; hd_required; hd_deprecated; hd_type_decl; } ->
  let type_decl = Typed.decl hd_type_decl in
  OpenApi.Header_object.mk
    ~description:hd_description
    ~required:hd_required
    ~deprecated:hd_deprecated
    ~schema:(openapi_schema_or_reference_of_type_decl reg_info type_decl)
    ()

and openapi_media_type_object_of_media_type :
  registry_info -> 't with_doc list -> 't media_type -> OpenApi.Header_object.media_type_object =
  fun reg_info samples { mt_type; mt_external_examples; } ->
  let _, {
      type_declarations;
      type_decl_environment_wrappers
    } = reg_info in
  let alias_ident_typemap =
    type_declarations
    |> foldl (fun acc info ->
           acc |> StringMap.add info.tdi_name info.tdi_decl
         ) StringMap.empty in
  let env0 = Type_decl_environment.{
        alias_ident_typemap;
        prim_ident_typemap = StringMap.empty;
             } in
  let env = type_decl_environment_wrappers |> List.foldl (|>) env0 in
  let type_decl = Typed.decl mt_type in
  let examples =
    (samples |> List.mapi (fun index (sample_val, sample_name) ->
        ((match sample_name with `docstr s -> s | `nodoc -> sprintf "example_%d" index),
         OpenApi.Example_object.mk
           ~value:(Codec.Json.to_json ~env mt_type sample_val)
           ()
         |> Either.left))) @
    (mt_external_examples |&> fun (ex_name, ex_url) ->
        (ex_name,
         OpenApi.Example_object.mk
           ~externalValue:ex_url
           ()
         |> Either.left)) in
  OpenApi.Header_object.media_type
    ~examples
    ~schema:(openapi_schema_or_reference_of_type_decl reg_info type_decl) ()

and openapi_external_documentation_object_of_external_doc :
  external_doc -> OpenApi.External_documentation_object.t =
  fun { ed_urlpath; ed_description = description; } ->
  OpenApi.External_documentation_object.mk ed_urlpath ?description

and openapi_schema_object_of_type_decl : type_decl -> OpenApi.Schema_object.t = gen_openapi_schema

and openapi_schema_or_reference_of_type_decl : registry_info -> type_decl -> (OpenApi.Schema_object.t, OpenApi.Reference_object.t) either =
  fun (_, tdcoll) td ->
    tdcoll.type_declarations |> List.exists (fun tdi ->
      let (Boxed ttd) = tdi.tdi_decl in
      let td' = Typed.decl ttd in
      td.td_name = td'.td_name
    )
    |> function
    | true ->
      Json_config.get_mangled_name_of_type td
      |> fst
      |> sprintf "#/components/schemas/%s"
      |> OpenApi.Reference_object.mk
      |> Either.right
    | false -> openapi_schema_object_of_type_decl td |> Either.left

module Internals = struct
  let openapi_components_object_of_type_decl_collection =
    openapi_components_object_of_type_decl_collection

  let openapi_paths_object_of_invocation_point_collection =
    openapi_paths_object_of_invocation_point_collection

  let openapi_path_item_object_of_invocation_point_info =
    openapi_path_item_object_of_invocation_point_info

  let openapi_request_body_object_of_request_body =
    openapi_request_body_object_of_request_body

  let openapi_response_object_of_response =
    openapi_response_object_of_response

  let openapi_header_object_of_header =
    openapi_header_object_of_header

  let openapi_media_type_object_of_media_type =
    openapi_media_type_object_of_media_type

  let openapi_external_documentation_object_of_external_doc =
    openapi_external_documentation_object_of_external_doc

  let openapi_schema_object_of_type_decl = openapi_schema_object_of_type_decl

  let openapi_schema_or_reference_of_type_decl = openapi_schema_or_reference_of_type_decl
end