From 14999d9684939008d8c0bfccc2bee27c1c95501d Mon Sep 17 00:00:00 2001 From: Konstantina Chremmou Date: Wed, 20 Dec 2023 22:07:44 +0000 Subject: [PATCH] Split the API reference markdown into smaller files and use templates to generate it. Signed-off-by: Konstantina Chremmou --- Makefile | 11 +- ocaml/doc/basics.md | 4 + ocaml/doc/vm-lifecycle.md | 4 + ocaml/doc/wire-protocol.md | 4 + ocaml/idl/autogen/api-ref-autogen.md | 12 + ocaml/idl/autogen/dune | 6 + ocaml/idl/datamodel.ml | 14 +- ocaml/idl/datamodel_errors.ml | 2 +- ocaml/idl/datamodel_host.ml | 2 +- ocaml/idl/datamodel_main.ml | 2 +- ocaml/idl/dune | 11 + ocaml/idl/markdown_backend.ml | 815 ++++++++++----------- ocaml/idl/templates/api_errors.mustache | 136 ++++ ocaml/idl/templates/class.mustache | 89 +++ ocaml/idl/templates/classes.mustache | 13 + ocaml/idl/templates/relationships.mustache | 19 + ocaml/idl/templates/toc.mustache | 15 + ocaml/idl/templates/types.mustache | 40 + 18 files changed, 749 insertions(+), 450 deletions(-) create mode 100644 ocaml/idl/autogen/api-ref-autogen.md create mode 100644 ocaml/idl/autogen/dune create mode 100644 ocaml/idl/templates/api_errors.mustache create mode 100644 ocaml/idl/templates/class.mustache create mode 100644 ocaml/idl/templates/classes.mustache create mode 100644 ocaml/idl/templates/relationships.mustache create mode 100644 ocaml/idl/templates/toc.mustache create mode 100644 ocaml/idl/templates/types.mustache diff --git a/Makefile b/Makefile index 819bd9a432b..5d44533d189 100644 --- a/Makefile +++ b/Makefile @@ -42,15 +42,20 @@ schema: dune runtest ocaml/idl doc: - dune build --profile=$(PROFILE) ocaml/idl/datamodel_main.exe +#html dune build --profile=$(PROFILE) -f @ocaml/doc/jsapigen mkdir -p $(XAPIDOC)/html cp -r _build/default/ocaml/doc/api $(XAPIDOC)/html cp _build/default/ocaml/doc/branding.js $(XAPIDOC)/html cp ocaml/doc/*.js ocaml/doc/*.html ocaml/doc/*.css $(XAPIDOC)/html - dune exec --profile=$(PROFILE) -- ocaml/idl/datamodel_main.exe -closed -markdown $(XAPIDOC)/markdown - cp ocaml/doc/*.dot ocaml/doc/doc-convert.sh $(XAPIDOC) +#markdown + dune build --profile=$(PROFILE) -f @ocaml/idl/markdowngen + mkdir -p $(XAPIDOC)/markdown + cp -r _build/default/ocaml/idl/autogen/*.md $(XAPIDOC)/markdown + cp -r _build/default/ocaml/idl/autogen/*.yml $(XAPIDOC)/markdown find ocaml/doc -name "*.md" -not -name "README.md" -exec cp {} $(XAPIDOC)/markdown/ \; +#other + cp ocaml/doc/*.dot ocaml/doc/doc-convert.sh $(XAPIDOC) # Build manpages, networkd generated these dune build --profile=$(PROFILE) -f @man diff --git a/ocaml/doc/basics.md b/ocaml/doc/basics.md index f659a795a14..9a75a16087b 100644 --- a/ocaml/doc/basics.md +++ b/ocaml/doc/basics.md @@ -1,3 +1,7 @@ +--- + layout: doc +--- + # API Basics This document defines the XenServer Management API - an interface for remotely diff --git a/ocaml/doc/vm-lifecycle.md b/ocaml/doc/vm-lifecycle.md index 31f31889f36..68664730617 100644 --- a/ocaml/doc/vm-lifecycle.md +++ b/ocaml/doc/vm-lifecycle.md @@ -1,3 +1,7 @@ +--- + layout: doc +--- + # VM Lifecycle The following diagram shows the states that a VM can be in diff --git a/ocaml/doc/wire-protocol.md b/ocaml/doc/wire-protocol.md index 260039ab495..e42a0f2da2c 100644 --- a/ocaml/doc/wire-protocol.md +++ b/ocaml/doc/wire-protocol.md @@ -1,3 +1,7 @@ +--- + layout: doc +--- + # Wire Protocol for Remote API Calls API calls are sent over a network to a Xen-enabled host using an RPC protocol. diff --git a/ocaml/idl/autogen/api-ref-autogen.md b/ocaml/idl/autogen/api-ref-autogen.md new file mode 100644 index 00000000000..7abf22b6872 --- /dev/null +++ b/ocaml/idl/autogen/api-ref-autogen.md @@ -0,0 +1,12 @@ +--- + layout: doc +--- + +# API Reference + +Version **@xapi-version@** + +- [Classes](@root@management-api/classes.html) +- [Relationships Between Classes](@root@management-api/relationships-between-classes.html) +- [Types](@root@management-api/types.html) +- [ErrorHandling](@root@management-api/api-ref-autogen-errors.html) diff --git a/ocaml/idl/autogen/dune b/ocaml/idl/autogen/dune new file mode 100644 index 00000000000..483a0dbdef8 --- /dev/null +++ b/ocaml/idl/autogen/dune @@ -0,0 +1,6 @@ +(alias + (name markdowngen) + (deps + (source_tree .) + ) +) \ No newline at end of file diff --git a/ocaml/idl/datamodel.ml b/ocaml/idl/datamodel.ml index aa19b1ab347..7f621c85e33 100644 --- a/ocaml/idl/datamodel.ml +++ b/ocaml/idl/datamodel.ml @@ -5985,7 +5985,7 @@ module DR_task = struct ) ; (Set String, "whitelist", "The devices to use for disaster recovery") ] - ~result:(Ref _dr_task, "The reference to the created task") + ~result:(Ref _dr_task, "The reference of the created DR_task") ~doc: "Create a disaster recovery task which will query the supplied list of \ devices" @@ -6202,7 +6202,7 @@ module Blob = struct } ] ~doc:"Create a placeholder for a binary blob" ~flags:[`Session] - ~result:(Ref _blob, "The reference to the created blob") + ~result:(Ref _blob, "The reference of the created blob") ~allowed_roles:_R_POOL_OP () let destroy = @@ -6889,7 +6889,8 @@ module GPU_group = struct ; param_default= Some (VMap []) } ] - ~result:(Ref _gpu_group, "") ~allowed_roles:_R_POOL_OP () + ~result:(Ref _gpu_group, "The reference of the created GPU_group") + ~allowed_roles:_R_POOL_OP () let destroy = call ~name:"destroy" @@ -7041,7 +7042,7 @@ module VGPU = struct ; param_default= Some (VRef null_ref) } ] - ~result:(Ref _vgpu, "reference to the newly created object") + ~result:(Ref _vgpu, "The reference of the created VGPU object") ~allowed_roles:_R_POOL_OP () let destroy = @@ -7356,7 +7357,7 @@ module PVS_proxy = struct let create = call ~name:"create" ~doc:"Configure a VM/VIF to use a PVS proxy" - ~result:(Ref _pvs_proxy, "the new PVS proxy") + ~result:(Ref _pvs_proxy, "The reference of the created PVS proxy") ~params: [ (Ref _pvs_site, "site", "PVS site that we proxy for") @@ -7626,7 +7627,8 @@ module USB_group = struct ; param_default= Some (VMap []) } ] - ~result:(Ref _usb_group, "") ~allowed_roles:_R_POOL_ADMIN () + ~result:(Ref _usb_group, "The reference of the created USB_group") + ~allowed_roles:_R_POOL_ADMIN () let destroy = call ~name:"destroy" ~lifecycle diff --git a/ocaml/idl/datamodel_errors.ml b/ocaml/idl/datamodel_errors.ml index 2aa67691774..58c143c2253 100644 --- a/ocaml/idl/datamodel_errors.ml +++ b/ocaml/idl/datamodel_errors.ml @@ -1455,7 +1455,7 @@ let _ = ~doc: "The requested update could not be found. Please upload the update \ again. This can occur when you run xe update-pool-clean before xe \ - update-apply. " + update-apply." () ; error Api_errors.update_pool_apply_failed ["hosts"] ~doc:"The update cannot be applied for the following host(s)." () ; diff --git a/ocaml/idl/datamodel_host.ml b/ocaml/idl/datamodel_host.ml index 672f34ea8c4..e535e1b566f 100644 --- a/ocaml/idl/datamodel_host.ml +++ b/ocaml/idl/datamodel_host.ml @@ -1368,7 +1368,7 @@ let set_power_on_mode = ; (Changed, rel_stockholm, "Removed iLO script") ] ~in_product_since:rel_midnight_ride - ~doc:"Set the power-on-mode, host, user and password " + ~doc:"Set the power-on-mode, host, user and password" ~params: [ (Ref _host, "self", "The host") diff --git a/ocaml/idl/datamodel_main.ml b/ocaml/idl/datamodel_main.ml index 77250738817..fa22d3b9d09 100644 --- a/ocaml/idl/datamodel_main.ml +++ b/ocaml/idl/datamodel_main.ml @@ -86,7 +86,7 @@ let _ = in if !markdown_mode then - Markdown_backend.all api !dirname ; + Markdown_backend.all api ; if !dirname <> "" then Unix.chdir !dirname ; if !dot_mode then diff --git a/ocaml/idl/dune b/ocaml/idl/dune index 3dfa75af8c4..713462e7ffa 100644 --- a/ocaml/idl/dune +++ b/ocaml/idl/dune @@ -30,6 +30,7 @@ (modules datamodel_main dot_backend dtd_backend markdown_backend) (libraries dune-build-info + mustache xapi-datamodel xapi-stdext-std xapi-stdext-pervasives @@ -37,6 +38,16 @@ ) ) +(rule + (alias markdowngen) + (deps + (:x datamodel_main.exe) + (source_tree templates) + ) + (package xapi-datamodel) + (action (run %{x} -closed -markdown)) +) + (test (name schematest) (modes exe) diff --git a/ocaml/idl/markdown_backend.ml b/ocaml/idl/markdown_backend.ml index edd95d95d50..c4bbd538fe1 100644 --- a/ocaml/idl/markdown_backend.ml +++ b/ocaml/idl/markdown_backend.ml @@ -15,7 +15,7 @@ open Printf open Datamodel_types open Datamodel_utils open Dm_api -open Xapi_stdext_pervasives.Pervasiveext +module Unixext = Xapi_stdext_unix.Unixext (*column widths for the autogenerated tables*) let col_width_15 = 15 @@ -28,6 +28,10 @@ let col_width_40 = 40 let col_width_70 = 70 +let destdir = "autogen" + +let templatesdir = "templates" + let pad_right x max_width = let length = String.length x in if String.length x < max_width then @@ -78,14 +82,6 @@ let escape s = let escaped_list = List.map esc_char sl in String.concat "" escaped_list -let is_prim_type = function - | String | Int | Float | Bool | DateTime -> - true - | _ -> - false - -let is_prim_opt_type = function None -> true | Some (ty, _) -> is_prim_type ty - let rec of_ty_verbatim = function | SecretString | String -> "string" @@ -152,231 +148,383 @@ let string_of_qualifier = function | RW -> "_RW_" -let is_removal_marker x = - match x with Lifecycle.Removed, _, _ -> true | _ -> false +let render_file (infile, outfile) json templates_dir dest_dir = + let templ = + Unixext.string_of_file (Filename.concat templates_dir infile) + |> Mustache.of_string + in + let rendered = Mustache.render templ json in + let io = open_out (Filename.concat dest_dir outfile) in + Fun.protect + (fun () -> output_string io rendered) + ~finally:(fun () -> close_out io) -let is_deprecation_marker x = - match x with Lifecycle.Deprecated, _, _ -> true | _ -> false +let generate_class cls = + let class_json = + `O + [ + ("class_name", `String (escape cls.name)) + ; ("class_descr", `String (escape cls.description)) + ; ("has_descr", `Bool (cls.description <> "")) + ; ("class_deprecated", `Bool (cls.obj_lifecycle.state = Deprecated_s)) + ; ("class_removed", `Bool (cls.obj_lifecycle.state = Removed_s)) + ; ("is_event", `Bool (String.lowercase_ascii cls.name = "event")) + ; ("has_fields", `Bool (Datamodel_utils.fields_of_obj cls <> [])) + ; ( "fields" + , `A + (cls + |> Datamodel_utils.fields_of_obj + |> List.sort (fun x y -> + compare_case_ins + (Datamodel_utils.wire_name_of_field x) + (Datamodel_utils.wire_name_of_field y) + ) + |> List.map (fun field -> + `O + [ + ( "field_name" + , `String + (pad_right + (escape (Datamodel_utils.wire_name_of_field field)) + col_width_20 + ) + ) + ; ( "field_type" + , `String + (pad_right + ("`" ^ of_ty_verbatim field.ty ^ "`") + col_width_20 + ) + ) + ; ( "field_ctor" + , `String + (pad_right + (string_of_qualifier field.qualifier) + col_width_15 + ) + ) + ; ( "field_descr" + , `String + (pad_right + (escape field.field_description) + col_width_40 + ) + ) + ; ( "field_deprecated" + , `Bool + (field.lifecycle.state = Deprecated_s + || cls.obj_lifecycle.state = Deprecated_s + ) + ) + ; ( "field_removed" + , `Bool + (field.lifecycle.state = Removed_s + || cls.obj_lifecycle.state = Removed_s + ) + ) + ] + ) + ) + ) + ; ("has_rpcs", `Bool (cls.messages <> [])) + ; ( "all_rpcs" + , `A + (cls.messages + |> List.sort (fun x y -> compare_case_ins x.msg_name y.msg_name) + |> List.map (fun msg -> + let is_event_from = + String.lowercase_ascii cls.name = "event" + && String.lowercase_ascii msg.msg_name = "from" + in + let rpc_param_csv = + msg.msg_params + |> List.map (fun p -> + of_ty_verbatim p.param_type ^ " " ^ p.param_name + ) + |> String.concat ", " + in + let error_codes_csv = + msg.msg_errors + |> List.map (fun x -> sprintf "`%s`" x.err_name) + |> String.concat ", " + in + let rbac x = + match x.msg_allowed_roles with + | Some y when y <> [] -> + List.hd (List.rev y) + | _ -> + "" + in + `O + [ + ("rpc_name_escaped", `String (escape msg.msg_name)) + ; ("rpc_name", `String msg.msg_name) + ; ("rpc_descr", `String (escape msg.msg_doc)) + ; ("rpc_has_descr", `Bool (msg.msg_doc <> "")) + ; ( "rpc_deprecated" + , `Bool + (msg.msg_lifecycle.state = Lifecycle.Deprecated_s + || cls.obj_lifecycle.state = Deprecated_s + ) + ) + ; ( "rpc_removed" + , `Bool + (msg.msg_lifecycle.state = Lifecycle.Removed_s + || cls.obj_lifecycle.state = Removed_s + ) + ) + ; ("returns_void", `Bool (msg.msg_result = None)) + ; ( "return_type" + , `String + ( if is_event_from then + "event batch" + else + of_ty_opt_verbatim msg.msg_result + ) + ) + ; ( "return_descr" + , `String (escape (desc_of_ty_opt msg.msg_result)) + ) + ; ("rpc_param_csv", `String rpc_param_csv) + ; ("has_rbac", `Bool (rbac msg <> "")) + ; ("min_role", `String (rbac msg)) + ; ("session", `Bool msg.msg_session) + ; ("has_rpc_params", `Bool (msg.msg_params <> [])) + ; ( "rpc_params" + , `A + (msg.msg_params + |> List.map (fun p -> + `O + [ + ( "param_name" + , `String + (pad_right (escape p.param_name) + col_width_30 + ) + ) + ; ( "param_type" + , `String + (pad_right + ("`" + ^ of_ty_verbatim p.param_type + ^ "`" + ) + col_width_30 + ) + ) + ; ( "param_descr" + , `String + (pad_right (escape p.param_doc) + col_width_40 + ) + ) + ] + ) + ) + ) + ; ("has_error_codes", `Bool (msg.msg_errors <> [])) + ; ("error_codes_csv", `String error_codes_csv) + ] + ) + ) + ) + ] + in + render_file + ("class.mustache", sprintf "class-%s.md" (String.lowercase_ascii cls.name)) + class_json templatesdir destdir -(* Make a markdown section for an API-specified message *) -let markdown_section_of_message printer obj ~is_class_deprecated - ~is_class_removed x = - let is_event_from = - String.lowercase_ascii obj.name = "event" - && String.lowercase_ascii x.msg_name = "from" +let generate_types system = + let type_comparer x y = + match (x, y) with + | Enum (a, _), Enum (b, _) -> + compare_case_ins a b + | _ -> + compare x y in - let return_type = of_ty_opt_verbatim x.msg_result in - printer (sprintf "#### RPC name: %s" (escape x.msg_name)) ; - printer "" ; - if x.msg_lifecycle.state = Lifecycle.Removed_s || is_class_removed then ( - printer "**This message is removed.**" ; - printer "" - ) else if - x.msg_lifecycle.state = Lifecycle.Deprecated_s || is_class_deprecated - then ( - printer "**This message is deprecated.**" ; - printer "" - ) ; - printer "_Overview:_" ; - printer "" ; - printer (escape x.msg_doc) ; - printer "" ; - printer "_Signature:_" ; - printer "" ; - printer "```" ; - let result = - if is_event_from then - "" - else - of_ty_opt_verbatim x.msg_result + let enums = + Types.of_objects system + |> List.filter (function Enum (_, _) -> true | _ -> false) + |> List.sort type_comparer in - printer - (sprintf "%s %s (%s)" result x.msg_name - (String.concat ", " - ((if x.msg_session then ["session ref session_id"] else []) - @ List.map - (fun p -> of_ty_verbatim p.param_type ^ " " ^ p.param_name) - x.msg_params - ) - ) - ) ; - printer "```" ; - printer "" ; - if x.msg_params <> [] then ( - printer "_Arguments:_" ; - printer "" ; - printer - "|type |name \ - |description |" ; - printer - "|:-----------------------------|:-----------------------------|:---------------------------------------|" ; - if x.msg_session then - printer - "|session ref |session_id \ - |Reference to a valid session |" ; - let get_param_row p = - sprintf "|`%s`|%s|%s|" - (pad_right (of_ty_verbatim p.param_type) (col_width_30 - 2)) - (pad_right (escape p.param_name) col_width_30) - (pad_right (escape p.param_doc) col_width_40) - in - List.iter (fun p -> printer (get_param_row p)) x.msg_params ; - printer "" - ) ; - let print_rbac y = - match y.msg_allowed_roles with - | Some yy when yy <> [] -> - printer ("_Minimum Role:_ " ^ List.hd (List.rev yy)) ; - printer "" - | _ -> - () + let types_json = + `O + [ + ( "enums" + , `A + (List.map + (function + | Enum (name, options) -> + `O + [ + ("enum", `String (pad_right name (col_width_40 - 5))) + ; ( "enum_options" + , `A + (options + |> List.sort (fun (x, _) (y, _) -> + compare_case_ins x y + ) + |> List.map (fun (n, c) -> + `O + [ + ( "option_name" + , `String + (pad_right + ("`" ^ n ^ "`") + col_width_40 + ) + ) + ; ( "option_descr" + , `String + (pad_right (escape c) col_width_40) + ) + ] + ) + ) + ) + ] + | _ -> + `Null + ) + enums + ) + ) + ] in - print_rbac x ; - printer - ("_Return Type:_" - ^ if is_event_from then " an event batch" else sprintf " `%s`" return_type - ) ; - printer "" ; - let descr = desc_of_ty_opt x.msg_result in - if descr <> "" then ( - printer (escape descr) ; - printer "" - ) ; - if x.msg_errors <> [] then ( - let error_codes = - List.map (fun err -> sprintf "`%s`" err.err_name) x.msg_errors - in - printer - (sprintf "_Possible Error Codes:_ %s" (String.concat ", " error_codes)) ; - printer "" - ) + render_file ("types.mustache", "types.md") types_json templatesdir destdir -let print_field_table_of_obj printer ~is_class_deprecated ~is_class_removed x = - printer (sprintf "### Fields for class: " ^ escape x.name) ; - printer "" ; - if x.contents = [] then - printer ("Class " ^ escape x.name ^ " has no fields.") - else ( - printer - "|Field |Type |Qualifier \ - |Description |" ; - printer - "|:-------------------|:-------------------|:--------------|:---------------------------------------|" ; - let print_field_content printer - ({qualifier; ty; field_description= description; _} as y) = - let wired_name = Datamodel_utils.wire_name_of_field y in - let descr = - ( if y.lifecycle.state = Removed_s || is_class_removed then - "**Removed**. " - else if y.lifecycle.state = Deprecated_s || is_class_deprecated then - "**Deprecated**. " - else - "" - ) - ^ escape description - in - printer - (sprintf "|%s|`%s`|%s|%s|" - (pad_right (escape wired_name) col_width_20) - (pad_right (of_ty_verbatim ty) (col_width_20 - 2)) - (pad_right (string_of_qualifier qualifier) col_width_15) - (pad_right descr col_width_40) - ) - in - x - |> Datamodel_utils.fields_of_obj - |> List.sort (fun x y -> - compare_case_ins - (Datamodel_utils.wire_name_of_field x) - (Datamodel_utils.wire_name_of_field y) - ) - |> List.iter (print_field_content printer) ; - if String.lowercase_ascii x.name = "event" then - printer - (sprintf "|%s|`%s`|%s|%s|" - (pad_right "snapshot" col_width_20) - (pad_right "" (col_width_20 - 2)) - (pad_right "_RO/runtime_" col_width_15) - (pad_right - "The record of the database object that was added, changed or \ - deleted" - col_width_40 - ) +let generate_relationships api = + let relations = relations_of_api api in + let relationships_json = + `O + [ + ( "relationships" + , `A + (List.map + (function + | ((a, a_field), (b, b_field)) as rel -> + let c = Relations.classify api rel in + let afield = "`" ^ a ^ "." ^ a_field ^ "`" in + let bfield = "`" ^ b ^ "." ^ b_field ^ "`" in + `O + [ + ( "a_field" + , `String (pad_right afield (col_width_40 - 2)) + ) + ; ( "b_field" + , `String (pad_right bfield (col_width_40 - 2)) + ) + ; ( "relationship" + , `String + (pad_right + (Relations.string_of_classification c) + col_width_15 + ) + ) + ] + ) + relations + ) ) - ) + ] + in + render_file + ("relationships.mustache", "relationships-between-classes.md") + relationships_json templatesdir destdir -let of_obj printer x = - printer (sprintf "## Class: %s" (escape x.name)) ; - printer "" ; - let is_class_removed = x.obj_lifecycle.state = Removed_s in - let is_class_deprecated = x.obj_lifecycle.state = Deprecated_s in - if is_class_removed then ( - printer "**This class is removed.**" ; - printer "" - ) else if is_class_deprecated then ( - printer "**This class is deprecated.**" ; - printer "" - ) ; - printer (escape x.description) ; - printer "" ; - print_field_table_of_obj printer ~is_class_deprecated ~is_class_removed x ; - printer "" ; - printer (sprintf "### RPCs associated with class: " ^ escape x.name) ; - printer "" ; - if x.messages = [] then ( - printer - (sprintf "Class %s has no additional RPCs associated with it." - (escape x.name) - ) ; - printer "" - ) else - x.messages - |> List.sort (fun x y -> compare_case_ins x.msg_name y.msg_name) - |> List.iter - (markdown_section_of_message printer x ~is_class_deprecated - ~is_class_removed - ) +let generate_classes system = + let classes_json = + `O + [ + ( "classes" + , `A + (List.map + (fun x -> + let notice y = + match y.obj_lifecycle.state with + | Removed_s -> + "**Removed**. " + | Deprecated_s -> + "**Deprecated**. " + | _ -> + "" + in + `O + [ + ("name", `String x.name) + ; ("name_lower", `String (String.lowercase_ascii x.name)) + ; ( "description" + , `String + (pad_right + (notice x ^ escape x.description) + col_width_70 + ) + ) + ] + ) + system + ) + ) + ] + in + render_file + ("classes.mustache", "classes.md") + classes_json templatesdir destdir -let print_enum printer = function - | Enum (name, options) -> - printer - (sprintf "|`enum %s`| |" - (pad_right name (col_width_40 - 7)) - ) ; - printer - "|:---------------------------------------|:---------------------------------------|" ; - let print_option (opt, description) = - printer - (sprintf "|`%s`|%s|" - (pad_right opt (col_width_40 - 2)) - (pad_right (escape description) col_width_40) - ) - in - options - |> List.sort (fun (x, _) (y, _) -> compare_case_ins x y) - |> List.iter print_option ; - printer "" - | _ -> - () +let generate_toc system = + let classes_json = + `O + [ + ( "classes" + , `A + (List.map + (fun x -> + `O + [ + ("name", `String x.name) + ; ("name_lower", `String (String.lowercase_ascii x.name)) + ] + ) + system + ) + ) + ] + in + render_file ("toc.mustache", "toc.yml") classes_json templatesdir destdir -let error_doc printer {err_name= name; err_params= params; err_doc= doc} = - printer (sprintf "### %s" (escape name)) ; - printer "" ; - printer (escape doc) ; - printer "" ; - if params = [] then - printer "No parameters." - else ( - printer "_Signature:_" ; - printer "" ; - printer "```" ; - printer (sprintf "%s(%s)" name (String.concat ", " params)) ; - printer "```" - ) ; - printer "" +let generate_errors () = + (* Sort the errors alphabetically, then generate one section per code. *) + let errs = + Hashtbl.fold (fun name err acc -> (name, err) :: acc) Datamodel.errors [] + |> List.sort (fun (n1, _) (n2, _) -> compare n1 n2) + |> List.split + |> snd + in + let error_json = + `O + [ + ( "errors" + , `A + (List.map + (fun {err_name; err_params; err_doc} -> + `O + [ + ("error_code", `String (escape err_name)) + ; ("error_code_unescaped", `String err_name) + ; ("error_description", `String (escape err_doc)) + ; ("parameters", `String (String.concat ", " err_params)) + ] + ) + errs + ) + ) + ] + in + render_file + ("api_errors.mustache", "api-ref-autogen-errors.md") + error_json templatesdir destdir -let print_classes api io = - let printer text = fprintf io "%s\n" text in +let all api = (* Remove private messages that are only used internally (e.g. get_record_internal) *) let api = Dm_api.filter @@ -390,219 +538,10 @@ let print_classes api io = let system = objects_of_api api |> List.sort (fun x y -> compare_case_ins x.name y.name) in - let relations = relations_of_api api in - printer - "# API Reference - Types and Classes\n\n\ - ## Classes\n\n\ - The following classes are defined:\n\n\ - |Name \ - |Description |\n\ - |:-------------------|:---------------------------------------------------------------------|" ; - let get_descr obj = - ( if obj.obj_lifecycle.state = Removed_s then - "**Removed**. " - else if obj.obj_lifecycle.state = Deprecated_s then - "**Deprecated**. " - else - "" - ) - ^ escape obj.description - in - List.iter - (fun obj -> - printer - (sprintf "|`%s`|%s|" - (pad_right obj.name (col_width_20 - 2)) - (pad_right (get_descr obj) col_width_70) - ) - ) - system ; - printer - "\n\ - ## Relationships Between Classes\n\n\ - Fields that are bound together are shown in the following table:\n\n\ - |_object.field_ \ - |_object.field_ |_relationship_ |\n\ - |:---------------------------------------|:---------------------------------------|:--------------|" ; - List.iter - (function - | ((a, a_field), (b, b_field)) as rel -> - let c = Relations.classify api rel in - let afield = a ^ "." ^ a_field in - let bfield = b ^ "." ^ b_field in - printer - (sprintf "|`%s`|`%s`|%s|" - (pad_right afield (col_width_40 - 2)) - (pad_right bfield (col_width_40 - 2)) - (pad_right (Relations.string_of_classification c) col_width_15) - ) - ) - relations ; - printer - "\n\ - The following figure represents bound fields (as specified above) \ - diagramatically, using crow's foot notation to specify one-to-one, \ - one-to-many or many-to-many relationships:\n\n\ - ![Class relationships](classes.png 'Class relationships')\n\n\ - ## Types\n\n\ - ### Primitives\n\n\ - The following primitive types are used to specify methods and fields in \ - the API Reference:\n\n\ - |Type |Description |\n\ - |:-------|:-------------------------------------------|\n\ - |string |text strings |\n\ - |int |64-bit integers |\n\ - |float |IEEE double-precision floating-point numbers|\n\ - |bool |boolean |\n\ - |datetime|date and timestamp |\n\n\ - ### Higher-order types\n\n\ - The following type constructors are used:\n\n\ - |Type \ - |Description |\n\ - |:-----------------|:-------------------------------------------------------|\n\ - |_c_ ref |reference to an object of class \ - _c_ |\n\ - |_t_ set |a set of elements of type \ - _t_ |\n\ - |(_a -> b_) map |a table mapping values of type _a_ to values \ - of type _b_|\n\n\ - ### Enumeration types\n\n\ - The following enumeration types are used:\n" ; - let type_comparer x y = - match (x, y) with - | Enum (a, _), Enum (b, _) -> - compare_case_ins a b - | _ -> - compare x y - in - Types.of_objects system - |> List.sort type_comparer - |> List.iter (print_enum printer) ; - List.iter (fun x -> of_obj printer x) system -let print_errors io = - let printer text = fprintf io "%s\n" text in - printer - "# API Reference - Error Handling\n\n\ - When a low-level transport error occurs, or a request is malformed at the \ - HTTP\n\ - or RPC level, the server may send an HTTP 500 error response, or the client\n\ - may simulate the same. The client must be prepared to handle these errors,\n\ - though they may be treated as fatal.\n\n\ - On the wire, these are transmitted in a form similar to this when using the\n\ - XML-RPC protocol:\n\n\ - ```\n\ - $curl -D - -X POST https://server -H 'Content-Type: application/xml' \\\n\ - > -d '\n\ - > \n\ - > session.logout\n\ - > '\n\ - HTTP/1.1 500 Internal Error\n\ - content-length: 297\n\ - content-type:text/html\n\ - connection:close\n\ - cache-control:no-cache, no-store\n\n\ -

HTTP 500 internal server error

An unexpected error \ - occurred;\n\ - \ please wait a while and try again. If the problem persists, please \ - contact your\n\ - \ support representative.

Additional information \ -

Xmlrpc.Parse_error(&quo\n\ - t;close_tag", "open_tag", _)\n\ - ```\n\n\ - When using the JSON-RPC protocol:\n\n\ - ```\n\ - $curl -D - -X POST https://server/jsonrpc -H 'Content-Type: \ - application/json' \\\n\ - > -d '{\n\ - > \"jsonrpc\": \"2.0\",\n\ - > \"method\": \"session.login_with_password\",\n\ - > \"id\": 0\n\ - > }'\n\ - HTTP/1.1 500 Internal Error\n\ - content-length: 308\n\ - content-type:text/html\n\ - connection:close\n\ - cache-control:no-cache, no-store\n\n\ -

HTTP 500 internal server error

An unexpected error \ - occurred;\n\ - \ please wait a while and try again. If the problem persists, please \ - contact your\n\ - \ support representative.

Additional information \ -

Jsonrpc.Malformed_metho\n\ - d_request("{jsonrpc=...,method=...,id=...}")\n\ - ```\n\n\ - All other failures are reported with a more structured error response, to\n\ - allow better automatic response to failures, proper internationalisation of\n\ - any error message, and easier debugging.\n\n\ - On the wire, these are transmitted like this when using the XML-RPC \ - protocol:\n\n\ - ```xml\n\ - \ \n\ - \ \n\ - \ Status\n\ - \ Failure\n\ - \ \n\ - \ \n\ - \ ErrorDescription\n\ - \ \n\ - \ \n\ - \ \n\ - \ MAP_DUPLICATE_KEY\n\ - \ Customer\n\ - \ eSpiel Inc.\n\ - \ eSpiel Incorporated\n\ - \ \n\ - \ \n\ - \ \n\ - \ \n\ - \ \n\ - ```\n\n\ - Note that `ErrorDescription` value is an array of string values. The\n\ - first element of the array is an error code; the remainder of the array are\n\ - strings representing error parameters relating to that code. In this case,\n\ - the client has attempted to add the mapping _Customer ->\n\ - eSpiel Incorporated_ to a Map, but it already contains the mapping\n\ - _Customer -> eSpiel Inc._, and so the request has failed.\n\n\ - When using the JSON-RPC protocol v2.0, the above error is transmitted as:\n\n\ - ```json\n\ - {\n\ - \ \"jsonrpc\": \"2.0\",\n\ - \ \"error\": {\n\ - \ \"code\": 1,\n\ - \ \"message\": \"MAP_DUPLICATE_KEY\",\n\ - \ \"data\": [\n\ - \ \"Customer\",\"eSpiel Inc.\",\"eSpiel Incorporated\"\n\ - \ ]\n\ - \ },\n\ - \ \"id\": 3\n\ - \ }\n\ - ```\n\n\ - Finally, when using the JSON-RPC protocol v1.0:\n\n\ - ```json\n\ - {\n\ - \ \"result\": null,\n\ - \ \"error\": [\n\ - \ \"MAP_DUPLICATE_KEY\",\"Customer\",\"eSpiel Inc.\",\"eSpiel \ - Incorporated\"\n\ - \ ],\n\ - \ \"id\": \"xyz\"\n\ - }\n\ - ```\n\n\ - Each possible error code is documented in the following section.\n\n\ - ## Error Codes\n" ; - (* Sort the errors alphabetically, then generate one section per code. *) - let errs = - Hashtbl.fold (fun name err acc -> (name, err) :: acc) Datamodel.errors [] - in - List.iter (error_doc printer) - (snd (List.split (List.sort (fun (n1, _) (n2, _) -> compare n1 n2) errs))) - -let all api destdir = - Xapi_stdext_unix.Unixext.mkdir_rec destdir 0o755 ; - let with_file filename f = - let io = open_out (Filename.concat destdir filename) in - finally (fun () -> f io) (fun () -> close_out io) - in - with_file "api-ref-autogen.md" (print_classes api) ; - with_file "api-ref-autogen-errors.md" print_errors + List.iter generate_class system ; + generate_classes system ; + generate_relationships api ; + generate_types system ; + generate_errors () ; + generate_toc system diff --git a/ocaml/idl/templates/api_errors.mustache b/ocaml/idl/templates/api_errors.mustache new file mode 100644 index 00000000000..e1a4b95fbaa --- /dev/null +++ b/ocaml/idl/templates/api_errors.mustache @@ -0,0 +1,136 @@ +--- + layout: doc +--- + +# Error Handling + +When a low-level transport error occurs, or a request is malformed at the HTTP +or RPC level, the server may send an HTTP 500 error response, or the client +may simulate the same. The client must be prepared to handle these errors, +though they may be treated as fatal. + +On the wire, these are transmitted in a form similar to this when using the +XML-RPC protocol: + +``` +$curl -D - -X POST https://server -H 'Content-Type: application/xml' \ +> -d ' +> +> session.logout +> ' +HTTP/1.1 500 Internal Error +content-length: 297 +content-type:text/html +connection:close +cache-control:no-cache, no-store + +

HTTP 500 internal server error

An unexpected error occurred; + please wait a while and try again. If the problem persists, please contact your + support representative.

Additional information

Xmlrpc.Parse_error(&quo +t;close_tag", "open_tag", _) +``` + +When using the JSON-RPC protocol: + +``` +$curl -D - -X POST https://server/jsonrpc -H 'Content-Type: application/json' \ +> -d '{ +> "jsonrpc": "2.0", +> "method": "session.login_with_password", +> "id": 0 +> }' +HTTP/1.1 500 Internal Error +content-length: 308 +content-type:text/html +connection:close +cache-control:no-cache, no-store + +

HTTP 500 internal server error

An unexpected error occurred; + please wait a while and try again. If the problem persists, please contact your + support representative.

Additional information

Jsonrpc.Malformed_metho +d_request("{jsonrpc=...,method=...,id=...}") +``` + +All other failures are reported with a more structured error response, to +allow better automatic response to failures, proper internationalisation of +any error message, and easier debugging. + +On the wire, these are transmitted like this when using the XML-RPC protocol: + +```xml + + + Status + Failure + + + ErrorDescription + + + + MAP_DUPLICATE_KEY + Customer + eSpiel Inc. + eSpiel Incorporated + + + + + +``` + +Note that `ErrorDescription` value is an array of string values. The +first element of the array is an error code; the remainder of the array are +strings representing error parameters relating to that code. In this case, +the client has attempted to add the mapping _Customer -> +eSpiel Incorporated_ to a Map, but it already contains the mapping +_Customer -> eSpiel Inc._, hence the request has failed. + +When using the JSON-RPC protocol v2.0, the above error is transmitted as: + +```json +{ + "jsonrpc": "2.0", + "error": { + "code": 1, + "message": "MAP_DUPLICATE_KEY", + "data": [ + "Customer", + "eSpiel Inc.", + "eSpiel Incorporated" + ] + }, + "id": 3 +} +``` + +Finally, when using the JSON-RPC protocol v1.0: + +```json +{ + "result": null, + "error": [ + "MAP_DUPLICATE_KEY", + "Customer", + "eSpiel Inc.", + "eSpiel Incorporated" + ], + "id": "xyz" +} +``` + +Each possible error code is documented in the following section. + +## Error Codes +{{#errors}} + +### {{{error_code}}} + +{{{error_description}}} + +_Signature:_ + +``` +{{{error_code_unescaped}}}({{parameters}}) +``` +{{/errors}} \ No newline at end of file diff --git a/ocaml/idl/templates/class.mustache b/ocaml/idl/templates/class.mustache new file mode 100644 index 00000000000..65c7c294d22 --- /dev/null +++ b/ocaml/idl/templates/class.mustache @@ -0,0 +1,89 @@ +--- + layout: doc +--- + +# Class: {{{class_name}}} +{{#class_deprecated}} + +**This class is deprecated.** +{{/class_deprecated}} +{{#class_removed}} + +**This class is removed.** +{{/class_removed}} +{{#has_descr}} + +{{{class_descr}}} +{{/has_descr}} + +## Fields for class: {{{class_name}}} +{{#has_fields}} + +|Field |Type |Qualifier |Description | +|:-------------------|:-------------------|:--------------|:---------------------------------------| +{{/has_fields}} +{{#fields}} +|{{{field_name}}}|{{{field_type}}}|{{{field_ctor}}}|{{#field_deprecated}}**Deprecated.** {{/field_deprecated}}{{#field_removed}}**Removed.** {{/field_removed}}{{{field_descr}}}| +{{/fields}} +{{#is_event}} +|snapshot |object record |_RO/runtime_ |The record of the database object that was added, changed or deleted| +{{/is_event}} +{{^has_fields}} + +Class {{{class_name}}} has no fields. +{{/has_fields}} + +## RPCs associated with class: {{{class_name}}} +{{#all_rpcs}} + +### RPC name: {{{rpc_name_escaped}}} +{{#rpc_deprecated}} + +**This message is deprecated.** +{{/rpc_deprecated}} +{{#rpc_removed}} + +**This message is removed.** +{{/rpc_removed}} + +_Overview:_ +{{#rpc_has_descr}} + +{{{rpc_descr}}} +{{/rpc_has_descr}} + +_Signature:_ + +``` +{{{return_type}}} {{{rpc_name}}} ({{#session}}session ref session_ref{{#has_rpc_params}}, {{/has_rpc_params}}{{/session}}{{{rpc_param_csv}}}) +``` + +_Arguments:_ + +|type |name |description | +|:-----------------------------|:-----------------------------|:---------------------------------------| +{{#session}} +|session ref |session_ref |Reference to a valid session | +{{/session}} +{{#rpc_params}} +|{{{param_type}}}|{{{param_name}}}|{{{param_descr}}}| +{{/rpc_params}} + +{{#has_rbac}} +_Minimum Role:_ {{min_role}} + +{{/has_rbac}} +_Return Type:_ `{{{return_type}}}` +{{^returns_void}} + +{{{return_descr}}} +{{/returns_void}} +{{#has_error_codes}} + +_Possible Error Codes:_ {{{error_codes_csv}}} +{{/has_error_codes}} +{{/all_rpcs}} +{{^has_rpcs}} + +Class {{{class_name}}} has no RPCs associated with it. +{{/has_rpcs}} \ No newline at end of file diff --git a/ocaml/idl/templates/classes.mustache b/ocaml/idl/templates/classes.mustache new file mode 100644 index 00000000000..3730d77d0bc --- /dev/null +++ b/ocaml/idl/templates/classes.mustache @@ -0,0 +1,13 @@ +--- + layout: doc +--- + +# Classes + +The following classes are defined: + +|Name |Description | +|:-------------------|:---------------------------------------------------------------------| +{{#classes}} +|[{{{name}}}](@root@management-api/class-{{{name_lower}}}.html)|{{{description}}}| +{{/classes}} diff --git a/ocaml/idl/templates/relationships.mustache b/ocaml/idl/templates/relationships.mustache new file mode 100644 index 00000000000..a8567f5c6be --- /dev/null +++ b/ocaml/idl/templates/relationships.mustache @@ -0,0 +1,19 @@ +--- + layout: doc +--- + +# Relationships Between Classes + +Fields that are bound together are shown in the following table: + +|_object.field_ |_object.field_ |_relationship_ | +|:---------------------------------------|:---------------------------------------|:--------------| +{{#relationships}} +|{{{a_field}}}|{{{b_field}}}|{{relationship}}| +{{/relationships}} + +The following figure represents bound fields (as specified above) +diagramatically, using crow's foot notation to specify one-to-one, +one-to-many, or many-to-many relationships: + +![Class relationships](classes.png 'Class relationships') \ No newline at end of file diff --git a/ocaml/idl/templates/toc.mustache b/ocaml/idl/templates/toc.mustache new file mode 100644 index 00000000000..01f81f0982f --- /dev/null +++ b/ocaml/idl/templates/toc.mustache @@ -0,0 +1,15 @@ +- title: API Reference + url: @root@api-ref-autogen.html + subfolderlist: + - title: Classes + url: @root@management-api/classes.html +{{#classes}} + - title: Class:{{{name}}} + url: @root@management-api/class-{{{name_lower}}}.html +{{/classes}} + - title: Relationships Between Classes + url: @root@management-api/relationships-between-classes.html + - title: Types + url: @root@management-api/types.html + - title: Error Handling + url: @root@management-api/api-ref-autogen-errors.html diff --git a/ocaml/idl/templates/types.mustache b/ocaml/idl/templates/types.mustache new file mode 100644 index 00000000000..197a8ca5a0e --- /dev/null +++ b/ocaml/idl/templates/types.mustache @@ -0,0 +1,40 @@ +--- + layout: doc +--- + +# Types + +## Primitives + +The following primitive types are used to specify methods and fields in +the API Reference: + +|Type |Description | +|:-------|:-------------------------------------------| +|string |text strings | +|int |64-bit integers | +|float |IEEE double-precision floating-point numbers| +|bool |boolean | +|datetime|date and timestamp | + +## Higher-order types + +The following type constructors are used: + +|Type |Description | +|:-----------------|:-------------------------------------------------------| +|_c_ ref |reference to an object of class _c_ | +|_t_ set |a set of elements of type _t_ | +|(_a -> b_) map |a table mapping values of type _a_ to values of type _b_| + +## Enumeration types + +The following enumeration types are used: +{{#enums}} + +|enum {{{enum}}}| | +|:---------------------------------------|:---------------------------------------| +{{#enum_options}} +|{{{option_name}}}|{{{option_descr}}}| +{{/enum_options}} +{{/enums}} \ No newline at end of file