(**************************************************************************) (* *) (* Copyright (c) 2014 - 2016. *) (* Dynamic Ledger Solutions, Inc. *) (* *) (* All rights reserved. No warranty, explicit or implicit, provided. *) (* *) (**************************************************************************) (* Tezos Command line interface - Generic JSON RPC interface *) open Lwt open Cli_entries open Json_schema (*-- Assisted, schema directed input fill in --------------------------------*) exception Erroneous_construct exception Unsupported_construct type input = { int : int -> int -> string option -> string list -> int Lwt.t ; float : string option -> string list -> float Lwt.t ; string : string option -> string list -> string Lwt.t ; bool : string option -> string list -> bool Lwt.t ; continue : string option -> string list -> bool Lwt.t ; display : string -> unit Lwt.t ; } (* generic JSON generation from a schema with callback for random or interactive filling *) let fill_in input schema = let rec element path { title ; kind }= match kind with | Integer { minimum ; maximum } -> let minimum = match minimum with | None -> min_int | Some (m, `Inclusive) -> int_of_float m | Some (m, `Exclusive) -> int_of_float m + 1 in let maximum = match maximum with | None -> max_int | Some (m, `Inclusive) -> int_of_float m | Some (m, `Exclusive) -> int_of_float m - 1 in input.int minimum maximum title path >>= fun i -> return (`Float (float i)) | Number _ -> input.float title path >>= fun f -> return (`Float f) | Boolean -> input.bool title path >>= fun f -> return (`Bool f) | String _ -> input.string title path >>= fun f -> return (`String f) | Combine ((One_of | Any_of), elts) -> let nb = List.length elts in input.int 0 (nb - 1) (Some "Select the schema to follow") path >>= fun n -> element path (List.nth elts n) | Combine ((All_of | Not), _) -> fail Unsupported_construct | Def_ref name -> return (`String (Json_query.json_pointer_of_path name)) | Id_ref _ | Ext_ref _ -> fail Unsupported_construct | Array (elts, _) -> let rec fill_loop acc n ls = match ls with | [] -> return acc | elt :: elts -> element (string_of_int n :: path) elt >>= fun json -> fill_loop (json :: acc) (succ n) elts in fill_loop [] 0 elts >>= fun acc -> return (`A (List.rev acc)) | Object { properties } -> let rec fill_loop acc ls = match ls with | [] -> return acc | (n, elt, _, _) :: elts -> element (n :: path) elt >>= fun json -> fill_loop ((n, json) :: acc) elts in fill_loop [] properties >>= fun acc -> return (`O (List.rev acc)) | Monomorphic_array (elt, specs) -> let rec fill_loop acc min n max = if n > max then return acc else element (string_of_int n :: path) elt >>= fun json -> (if n < min then return true else input.continue title path) >>= function | true -> fill_loop (json :: acc) min (succ n) max | false -> return (json :: acc) in let max = match specs.max_items with None -> max_int | Some m -> m in fill_loop [] specs.min_items 0 max >>= fun acc -> return (`A (List.rev acc)) | Any -> fail Unsupported_construct | Dummy -> fail Unsupported_construct | Null -> return `Null in element [] (Json_schema.root schema) let random_fill_in schema = let display _ = return () in let int min max _ _ = let max = Int64.of_int max and min = Int64.of_int min in let range = Int64.sub max min in let random_int64 = Int64.add (Random.int64 range) min in return (Int64.to_int random_int64) in let string _title _ = return "" in let float _ _ = return (Random.float infinity) in let bool _ _ = return (Random.int 2 = 0) in let continue _ _ = return (Random.int 4 = 0) in catch (fun () -> fill_in { int ; float ; string ; bool ; display ; continue } schema >>= fun json -> return (Ok json)) (fun e -> let msg = Printf.sprintf "Fill-in failed %s\n%!" (Printexc.to_string e) in return (Error msg)) let editor_fill_in schema = let tmp = Filename.temp_file "tezos_rpc_call_" ".json" in let rec init () = (* write a temp file with instructions *) random_fill_in schema >>= function | Error msg -> return (Error msg) | Ok json -> Lwt_io.(with_file Output tmp (fun fp -> write_line fp (Data_encoding_ezjsonm.to_string json))) >>= fun () -> edit () and edit () = (* launch the user's editor on it *) let editor_cmd = try let ed = Sys.getenv "EDITOR" in Lwt_process.shell (ed ^ " " ^ tmp) with Not_found -> try let ed = Sys.getenv "VISUAL" in Lwt_process.shell (ed ^ " " ^ tmp) with Not_found -> if Sys.win32 then (* TODO: I have no idea what I'm doing here *) ("", [| "notepad.exe" ; tmp |]) else (* TODO: vi on MacOSX ? *) ("", [| "nano" ; tmp |]) in (Lwt_process.open_process_none editor_cmd) # status >>= function | Unix.WEXITED 0 -> reread () >>= fun json -> delete () >>= fun () -> return json | Unix.WSIGNALED x | Unix.WSTOPPED x | Unix.WEXITED x -> let msg = Printf.sprintf "FAILED %d \n%!" x in delete () >>= fun () -> return (Error msg) and reread () = (* finally reread the file *) Lwt_io.(with_file Input tmp (fun fp -> read fp)) >>= fun text -> match Data_encoding_ezjsonm.from_string text with | Ok r -> return (Ok r) | Error msg -> return (Error (Printf.sprintf "bad input: %s" msg)) and delete () = (* and delete the temp file *) Lwt_unix.unlink tmp in init () (*-- Nice list display ------------------------------------------------------*) module StringMap = Map.Make(String) let rec count = let open RPC.Description in function | Dynamic _ -> 1 | Static { service ; subdirs } -> let service = match service with | None -> 0 | Some _ -> 1 in let subdirs = match subdirs with | None -> 0 | Some (Suffixes subdirs) -> StringMap.fold (fun _ t r -> r + count t) subdirs 0 | Some (Arg (_, subdir)) -> count subdir in service + subdirs (*-- Commands ---------------------------------------------------------------*) let list url cctxt = let args = Utils.split '/' url in Client_node_rpcs.describe cctxt ~recurse:true args >>= fun tree -> let open RPC.Description in let collected_args = ref [] in let collect arg = if not (arg.RPC.Arg.descr <> None && List.mem arg !collected_args) then collected_args := arg :: !collected_args in let display_paragraph ppf description = Format.fprintf ppf "@, @[%a@]" (fun ppf words -> List.iter (Format.fprintf ppf "%s@ ") words) (Utils.split ' ' description) in let display_arg ppf arg = match arg.RPC.Arg.descr with | None -> Format.fprintf ppf "%s" arg.RPC.Arg.name | Some descr -> Format.fprintf ppf "<%s>%a" arg.RPC.Arg.name display_paragraph descr in let display_service ppf (_path, tpath, service) = Format.fprintf ppf "- /%s" (String.concat "/" tpath) ; match service.description with | None | Some "" -> () | Some description -> display_paragraph ppf description in let rec display ppf (path, tpath, tree) = match tree with | Dynamic description -> begin Format.fprintf ppf "- /%s " (String.concat "/" tpath) ; match description with | None | Some "" -> () | Some description -> display_paragraph ppf description end | Static { service = None ; subdirs = None } -> () | Static { service = Some service ; subdirs = None } -> display_service ppf (path, tpath, service) | Static { service ; subdirs = Some (Suffixes subdirs) } -> begin match service, StringMap.bindings subdirs with | None, [] -> () | None, [ n, solo ] -> display ppf (path @ [ n ], tpath @ [ n ], solo) | None, items when count tree >= 3 && path <> [] -> Format.fprintf ppf "@[+ %s/@,%a@]" (String.concat "/" path) (display_list tpath) items | Some service, items when count tree >= 3 && path <> [] -> Format.fprintf ppf "@[+ %s@,%a@,%a@]" (String.concat "/" path) display_service (path, tpath, service) (display_list tpath) items | None, (n, t) :: items -> Format.fprintf ppf "%a" display (path @ [ n ], tpath @ [ n ], t) ; List.iter (fun (n, t) -> Format.fprintf ppf "@,%a" display (path @ [ n ], tpath @ [ n ], t)) items | Some service, items -> display_service ppf (path, tpath, service) ; List.iter (fun (n, t) -> Format.fprintf ppf "@,%a" display (path @ [ n ], tpath @ [ n ], t)) items end | Static { service = None ; subdirs = Some (Arg (arg, solo)) } -> collect arg ; let name = Printf.sprintf "<%s>" arg.RPC.Arg.name in display ppf (path @ [ name ], tpath @ [ name ], solo) | Static { service = Some service ; subdirs = Some (Arg (arg, solo)) } -> collect arg ; display_service ppf (path, tpath, service) ; Format.fprintf ppf "@," ; let name = Printf.sprintf "<%s>" arg.RPC.Arg.name in display ppf (path @ [ name ], tpath @ [ name ], solo) and display_list tpath = Format.pp_print_list (fun ppf (n,t) -> display ppf ([ n ], tpath @ [ n ], t)) in cctxt.message "@ @[Available services:@ @ %a@]@." display (args, args, tree) >>= fun () -> if !collected_args <> [] then cctxt.message "@,@[Dynamic parameter description:@ @ %a@]@." (Format.pp_print_list display_arg) !collected_args else Lwt.return () let schema url cctxt = let args = Utils.split '/' url in let open RPC.Description in Client_node_rpcs.describe cctxt ~recurse:false args >>= function | Static { service = Some { input ; output } } -> cctxt.message "Input schema:\n%s\nOutput schema:\n%s\n%!" (Data_encoding_ezjsonm.to_string (Json_schema.to_json input)) (Data_encoding_ezjsonm.to_string (Json_schema.to_json output)) | _ -> cctxt.message "No service found at this URL (but this is a valid prefix)\n%!" let fill_in schema = let open Json_schema in match (root schema).kind with | Null -> Lwt.return (Ok `Null) | Any | Object { properties = [] } -> Lwt.return (Ok (`O [])) | _ -> editor_fill_in schema let call url cctxt = let args = Utils.split '/' url in let open RPC.Description in Client_node_rpcs.describe cctxt ~recurse:false args >>= function | Static { service = Some { input } } -> begin fill_in input >>= function | Error msg -> cctxt.error "%s" msg | Ok json -> Client_node_rpcs.get_json cctxt args json >>= fun json -> cctxt.message "Output:\n%s\n%!" (Data_encoding_ezjsonm.to_string json) end | _ -> cctxt.message "No service found at this URL (but this is a valid prefix)\n%!" let call_with_json url json (cctxt: Client_commands.context) = let args = Utils.split '/' url in match Data_encoding_ezjsonm.from_string json with | Error err -> cctxt.error "Failed to parse the proviede json: %s\n%!" err | Ok json -> let open RPC.Description in Client_node_rpcs.get_json cctxt args json >>= fun json -> cctxt.message "Output:\n%s\n%!" (Data_encoding_ezjsonm.to_string json) let group = { Cli_entries.name = "rpc" ; title = "Commands for the low level RPC layer" } let commands = [ command ~desc: "list all understood protocol versions" (fixed [ "list" ; "versions" ]) (fun cctxt -> Lwt_list.iter_s (fun (ver, _) -> cctxt.Client_commands.message "%a" Protocol_hash.pp_short ver) (Client_commands.get_versions ())) ; command ~group ~desc: "list available RPCs (low level command for advanced users)" (prefixes [ "rpc" ; "list" ] @@ stop) (list "/"); command ~group ~desc: "list available RPCs (low level command for advanced users)" (prefixes [ "rpc" ; "list" ] @@ string ~name:"url" ~desc: "the RPC's prefix to be described" @@ stop) list ; command ~group ~desc: "get the schemas of an RPC" (prefixes [ "rpc" ; "schema" ] @@ string ~name: "url" ~desc: "the RPC's URL" @@ stop) schema ; command ~group ~desc: "call an RPC (low level command for advanced users)" (prefixes [ "rpc" ; "call" ] @@ string ~name: "url" ~desc: "the RPC's URL" @@ stop) call ; command ~group ~desc: "call an RPC (low level command for advanced users)" (prefixes [ "rpc" ; "call" ] @@ string ~name: "url" ~desc: "the RPC's URL" @@ prefix "with" @@ string ~name:"" ~desc:"" @@ stop) call_with_json ]