diff --git a/CMakeLists.txt b/CMakeLists.txt index c01d00289f3..eabaf703628 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -250,6 +250,11 @@ if(BUILD_TESTS) json_schema ${CMAKE_CURRENT_SOURCE_DIR}/src/ds/test/json_schema.cpp ) + add_unit_test( + openapi_test ${CMAKE_CURRENT_SOURCE_DIR}/src/ds/test/openapi.cpp + ) + target_link_libraries(openapi_test PRIVATE http_parser.host) + add_unit_test( logger_json_test ${CMAKE_CURRENT_SOURCE_DIR}/src/ds/test/logger_json_test.cpp @@ -265,7 +270,7 @@ if(BUILD_TESTS) ) use_client_mbedtls(kv_test) target_link_libraries( - kv_test PRIVATE ${CMAKE_THREAD_LIBS_INIT} secp256k1.host + kv_test PRIVATE ${CMAKE_THREAD_LIBS_INIT} secp256k1.host http_parser.host ) add_unit_test( @@ -311,6 +316,7 @@ if(BUILD_TESTS) target_include_directories(history_test PRIVATE ${EVERCRYPT_INC}) target_link_libraries( history_test PRIVATE ${CRYPTO_LIBRARY} evercrypt.host secp256k1.host + http_parser.host ) add_unit_test( @@ -330,7 +336,9 @@ if(BUILD_TESTS) historical_queries_test ${CMAKE_CURRENT_SOURCE_DIR}/src/node/test/historical_queries.cpp ) - target_link_libraries(historical_queries_test PRIVATE secp256k1.host) + target_link_libraries( + historical_queries_test PRIVATE secp256k1.host http_parser.host + ) add_unit_test( snapshot_test ${CMAKE_CURRENT_SOURCE_DIR}/src/node/test/snapshot.cpp @@ -427,7 +435,7 @@ if(BUILD_TESTS) ${CMAKE_CURRENT_SOURCE_DIR}/src/lua_interp/test/lua_kv.cpp ) target_include_directories(lua_test PRIVATE ${LUA_DIR}) - target_link_libraries(lua_test PRIVATE lua.host) + target_link_libraries(lua_test PRIVATE lua.host http_parser.host) add_unit_test( merkle_test ${CMAKE_CURRENT_SOURCE_DIR}/src/node/test/merkle_test.cpp diff --git a/doc/schemas/ack/update_state_digest_POST_result.json b/doc/schemas/ack/update_state_digest_POST_result.json index c733d7ac7e0..1c7ea28845c 100644 --- a/doc/schemas/ack/update_state_digest_POST_result.json +++ b/doc/schemas/ack/update_state_digest_POST_result.json @@ -1,5 +1,4 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", "properties": { "signed_req": { "properties": { diff --git a/doc/schemas/ack_POST_params.json b/doc/schemas/ack_POST_params.json index fb3c0178a2c..0aafe05368b 100644 --- a/doc/schemas/ack_POST_params.json +++ b/doc/schemas/ack_POST_params.json @@ -1,5 +1,4 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", "properties": { "state_digest": { "items": { diff --git a/doc/schemas/ack_POST_result.json b/doc/schemas/ack_POST_result.json index 9c1c4ab18cd..7268be8c791 100644 --- a/doc/schemas/ack_POST_result.json +++ b/doc/schemas/ack_POST_result.json @@ -1,5 +1,4 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", "title": "ack/result", "type": "boolean" } \ No newline at end of file diff --git a/doc/schemas/api/schema_GET_params.json b/doc/schemas/api/schema_GET_params.json index ac6a17363a7..bde9544121e 100644 --- a/doc/schemas/api/schema_GET_params.json +++ b/doc/schemas/api/schema_GET_params.json @@ -1,5 +1,4 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", "properties": { "method": { "type": "string" diff --git a/doc/schemas/api/schema_GET_result.json b/doc/schemas/api/schema_GET_result.json index 1351f0d663d..000c6590883 100644 --- a/doc/schemas/api/schema_GET_result.json +++ b/doc/schemas/api/schema_GET_result.json @@ -1,11 +1,10 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", "properties": { "params_schema": { - "$ref": "http://json-schema.org/draft-07/schema#" + "type": "object" }, "result_schema": { - "$ref": "http://json-schema.org/draft-07/schema#" + "type": "object" } }, "required": [ diff --git a/doc/schemas/api_GET_result.json b/doc/schemas/api_GET_result.json deleted file mode 100644 index aa1e80bef5b..00000000000 --- a/doc/schemas/api_GET_result.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "properties": { - "endpoints": { - "items": { - "properties": { - "path": { - "type": "string" - }, - "verb": { - "type": "string" - } - }, - "required": [ - "verb", - "path" - ], - "type": "object" - }, - "type": "array" - } - }, - "required": [ - "endpoints" - ], - "title": "api/result", - "type": "object" -} \ No newline at end of file diff --git a/doc/schemas/app_openapi.json b/doc/schemas/app_openapi.json new file mode 100644 index 00000000000..8358403b8de --- /dev/null +++ b/doc/schemas/app_openapi.json @@ -0,0 +1,971 @@ +{ + "components": { + "schemas": { + "CallerInfo": { + "properties": { + "caller_id": { + "$ref": "#/components/schemas/uint64" + } + }, + "required": [ + "caller_id" + ], + "type": "object" + }, + "CodeStatus": { + "enum": [ + "ACCEPTED", + "RETIRED" + ] + }, + "EndpointMetrics__Metric": { + "properties": { + "calls": { + "$ref": "#/components/schemas/uint64" + }, + "errors": { + "$ref": "#/components/schemas/uint64" + }, + "failures": { + "$ref": "#/components/schemas/uint64" + } + }, + "required": [ + "calls", + "errors", + "failures" + ], + "type": "object" + }, + "EndpointMetrics__Out": { + "properties": { + "metrics": { + "$ref": "#/components/schemas/named_named_EndpointMetrics__Metric" + } + }, + "required": [ + "metrics" + ], + "type": "object" + }, + "GetCode__Out": { + "properties": { + "versions": { + "$ref": "#/components/schemas/GetCode__Version_array" + } + }, + "required": [ + "versions" + ], + "type": "object" + }, + "GetCode__Version": { + "properties": { + "digest": { + "$ref": "#/components/schemas/string" + }, + "status": { + "$ref": "#/components/schemas/CodeStatus" + } + }, + "required": [ + "digest", + "status" + ], + "type": "object" + }, + "GetCode__Version_array": { + "items": { + "$ref": "#/components/schemas/GetCode__Version" + }, + "type": "array" + }, + "GetCommit__Out": { + "properties": { + "seqno": { + "$ref": "#/components/schemas/int64" + }, + "view": { + "$ref": "#/components/schemas/int64" + } + }, + "required": [ + "view", + "seqno" + ], + "type": "object" + }, + "GetMetrics__HistogramResults": { + "properties": { + "buckets": { + "$ref": "#/components/schemas/json" + }, + "high": { + "$ref": "#/components/schemas/int32" + }, + "low": { + "$ref": "#/components/schemas/int32" + }, + "overflow": { + "$ref": "#/components/schemas/uint64" + }, + "underflow": { + "$ref": "#/components/schemas/uint64" + } + }, + "required": [ + "low", + "high", + "overflow", + "underflow", + "buckets" + ], + "type": "object" + }, + "GetMetrics__Out": { + "properties": { + "histogram": { + "$ref": "#/components/schemas/GetMetrics__HistogramResults" + }, + "tx_rates": { + "$ref": "#/components/schemas/json" + } + }, + "required": [ + "histogram", + "tx_rates" + ], + "type": "object" + }, + "GetNetworkInfo__NodeInfo": { + "properties": { + "host": { + "$ref": "#/components/schemas/string" + }, + "node_id": { + "$ref": "#/components/schemas/uint64" + }, + "port": { + "$ref": "#/components/schemas/string" + } + }, + "required": [ + "node_id", + "host", + "port" + ], + "type": "object" + }, + "GetNetworkInfo__NodeInfo_array": { + "items": { + "$ref": "#/components/schemas/GetNetworkInfo__NodeInfo" + }, + "type": "array" + }, + "GetNetworkInfo__Out": { + "properties": { + "nodes": { + "$ref": "#/components/schemas/GetNetworkInfo__NodeInfo_array" + }, + "primary_id": { + "$ref": "#/components/schemas/uint64" + } + }, + "required": [ + "nodes", + "primary_id" + ], + "type": "object" + }, + "GetNodesByRPCAddress__NodeInfo": { + "properties": { + "node_id": { + "$ref": "#/components/schemas/uint64" + }, + "status": { + "$ref": "#/components/schemas/NodeStatus" + } + }, + "required": [ + "node_id", + "status" + ], + "type": "object" + }, + "GetNodesByRPCAddress__NodeInfo_array": { + "items": { + "$ref": "#/components/schemas/GetNodesByRPCAddress__NodeInfo" + }, + "type": "array" + }, + "GetNodesByRPCAddress__Out": { + "properties": { + "nodes": { + "$ref": "#/components/schemas/GetNodesByRPCAddress__NodeInfo_array" + } + }, + "required": [ + "nodes" + ], + "type": "object" + }, + "GetPrimaryInfo__Out": { + "properties": { + "current_view": { + "$ref": "#/components/schemas/int64" + }, + "primary_host": { + "$ref": "#/components/schemas/string" + }, + "primary_id": { + "$ref": "#/components/schemas/uint64" + }, + "primary_port": { + "$ref": "#/components/schemas/string" + } + }, + "required": [ + "primary_id", + "primary_host", + "primary_port", + "current_view" + ], + "type": "object" + }, + "GetReceipt__Out": { + "properties": { + "receipt": { + "$ref": "#/components/schemas/uint8_array" + } + }, + "required": [ + "receipt" + ], + "type": "object" + }, + "GetSchema__Out": { + "properties": { + "params_schema": { + "$ref": "#/components/schemas/json_schema" + }, + "result_schema": { + "$ref": "#/components/schemas/json_schema" + } + }, + "required": [ + "params_schema", + "result_schema" + ], + "type": "object" + }, + "GetTxStatus__Out": { + "properties": { + "status": { + "$ref": "#/components/schemas/TxStatus" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "LoggingGet__Out": { + "properties": { + "msg": { + "$ref": "#/components/schemas/string" + } + }, + "required": [ + "msg" + ], + "type": "object" + }, + "LoggingRecord__In": { + "properties": { + "id": { + "$ref": "#/components/schemas/uint64" + }, + "msg": { + "$ref": "#/components/schemas/string" + } + }, + "required": [ + "id", + "msg" + ], + "type": "object" + }, + "NodeStatus": { + "enum": [ + "PENDING", + "TRUSTED", + "RETIRED" + ] + }, + "TxStatus": { + "enum": [ + "UNKNOWN", + "PENDING", + "COMMITTED", + "INVALID" + ] + }, + "VerifyReceipt__In": { + "properties": { + "receipt": { + "$ref": "#/components/schemas/uint8_array" + } + }, + "required": [ + "receipt" + ], + "type": "object" + }, + "VerifyReceipt__Out": { + "properties": { + "valid": { + "$ref": "#/components/schemas/boolean" + } + }, + "required": [ + "valid" + ], + "type": "object" + }, + "boolean": { + "type": "boolean" + }, + "int32": { + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "int64": { + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + }, + "json": {}, + "json_schema": { + "type": "object" + }, + "named_EndpointMetrics__Metric": { + "additionalProperties": { + "$ref": "#/components/schemas/EndpointMetrics__Metric" + }, + "type": "object" + }, + "named_named_EndpointMetrics__Metric": { + "additionalProperties": { + "$ref": "#/components/schemas/named_EndpointMetrics__Metric" + }, + "type": "object" + }, + "string": { + "type": "string" + }, + "uint64": { + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + }, + "uint8": { + "maximum": 255, + "minimum": 0, + "type": "integer" + }, + "uint8_array": { + "items": { + "$ref": "#/components/schemas/uint8" + }, + "type": "array" + } + } + }, + "info": { + "description": "This CCF sample app implements a simple logging application, securely recording messages at client-specified IDs. It demonstrates most of the features available to CCF apps.", + "title": "CCF Sample Logging App", + "version": "0.0.1" + }, + "openapi": "3.0.0", + "paths": { + "/api": { + "get": { + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/json" + } + } + }, + "description": "Default response description" + } + } + } + }, + "/api/schema": { + "get": { + "parameters": [ + { + "in": "query", + "name": "method", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetSchema__Out" + } + } + }, + "description": "Default response description" + } + } + } + }, + "/code": { + "get": { + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetCode__Out" + } + } + }, + "description": "Default response description" + } + } + } + }, + "/commit": { + "get": { + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetCommit__Out" + } + } + }, + "description": "Default response description" + } + } + } + }, + "/endpoint_metrics": { + "get": { + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EndpointMetrics__Out" + } + } + }, + "description": "Default response description" + } + } + } + }, + "/local_tx": { + "get": { + "parameters": [ + { + "in": "query", + "name": "seqno", + "required": false, + "schema": { + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + } + }, + { + "in": "query", + "name": "view", + "required": false, + "schema": { + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetTxStatus__Out" + } + } + }, + "description": "Default response description" + } + } + } + }, + "/log/private": { + "delete": { + "parameters": [ + { + "in": "query", + "name": "id", + "required": false, + "schema": { + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/boolean" + } + } + }, + "description": "Default response description" + } + } + }, + "get": { + "parameters": [ + { + "in": "query", + "name": "id", + "required": false, + "schema": { + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LoggingGet__Out" + } + } + }, + "description": "Default response description" + } + } + }, + "post": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LoggingRecord__In" + } + } + }, + "description": "Auto-generated request body schema" + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/boolean" + } + } + }, + "description": "Default response description" + } + } + } + }, + "/log/private/admin_only": { + "post": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LoggingRecord__In" + } + } + }, + "description": "Auto-generated request body schema" + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/boolean" + } + } + }, + "description": "Default response description" + } + } + } + }, + "/log/private/anonymous": { + "post": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LoggingRecord__In" + } + } + }, + "description": "Auto-generated request body schema" + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/boolean" + } + } + }, + "description": "Default response description" + } + } + } + }, + "/log/private/raw_text/{id}": { + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/log/public": { + "delete": { + "parameters": [ + { + "in": "query", + "name": "id", + "required": false, + "schema": { + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/boolean" + } + } + }, + "description": "Default response description" + } + } + }, + "get": { + "parameters": [ + { + "in": "query", + "name": "id", + "required": false, + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "msg": { + "type": "string" + } + }, + "required": [ + "msg" + ], + "title": "log/public/result", + "type": "object" + } + } + }, + "description": "Default response description" + } + } + }, + "post": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "properties": { + "id": { + "type": "number" + }, + "msg": { + "type": "string" + } + }, + "required": [ + "id", + "msg" + ], + "title": "log/public/params", + "type": "object" + } + } + } + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "title": "log/public/result", + "type": "boolean" + } + } + }, + "description": "Default response description" + } + } + } + }, + "/metrics": { + "get": { + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetMetrics__Out" + } + } + }, + "description": "Default response description" + } + } + } + }, + "/network_info": { + "get": { + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetNetworkInfo__Out" + } + } + }, + "description": "Default response description" + } + } + } + }, + "/node/ids": { + "get": { + "parameters": [ + { + "in": "query", + "name": "host", + "required": false, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "port", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetNodesByRPCAddress__Out" + } + } + }, + "description": "Default response description" + } + } + } + }, + "/primary_info": { + "get": { + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetPrimaryInfo__Out" + } + } + }, + "description": "Default response description" + } + } + } + }, + "/receipt": { + "get": { + "parameters": [ + { + "in": "query", + "name": "commit", + "required": false, + "schema": { + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetReceipt__Out" + } + } + }, + "description": "Default response description" + } + } + } + }, + "/receipt/verify": { + "post": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VerifyReceipt__In" + } + } + }, + "description": "Auto-generated request body schema" + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VerifyReceipt__Out" + } + } + }, + "description": "Default response description" + } + } + } + }, + "/tx": { + "get": { + "parameters": [ + { + "in": "query", + "name": "seqno", + "required": false, + "schema": { + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + } + }, + { + "in": "query", + "name": "view", + "required": false, + "schema": { + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetTxStatus__Out" + } + } + }, + "description": "Default response description" + } + } + } + }, + "/user_id": { + "get": { + "parameters": [ + { + "in": "query", + "name": "cert", + "required": false, + "schema": { + "items": { + "maximum": 255, + "minimum": 0, + "type": "integer" + }, + "type": "array" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CallerInfo" + } + } + }, + "description": "Default response description" + } + } + } + } + }, + "servers": [ + { + "url": "/app" + } + ] +} \ No newline at end of file diff --git a/doc/schemas/code_GET_result.json b/doc/schemas/code_GET_result.json index 2e76eae947a..f667b931693 100644 --- a/doc/schemas/code_GET_result.json +++ b/doc/schemas/code_GET_result.json @@ -1,5 +1,4 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", "properties": { "versions": { "items": { diff --git a/doc/schemas/commit_GET_result.json b/doc/schemas/commit_GET_result.json index 2f3be75f8b0..d90ed03a33d 100644 --- a/doc/schemas/commit_GET_result.json +++ b/doc/schemas/commit_GET_result.json @@ -1,5 +1,4 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", "properties": { "seqno": { "maximum": 9223372036854775807, diff --git a/doc/schemas/endpoint_metrics_GET_result.json b/doc/schemas/endpoint_metrics_GET_result.json index 914b658992e..3f4d34d79f5 100644 --- a/doc/schemas/endpoint_metrics_GET_result.json +++ b/doc/schemas/endpoint_metrics_GET_result.json @@ -1,52 +1,35 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", "properties": { "metrics": { - "items": { - "items": [ - { - "type": "string" - }, - { - "items": { - "items": [ - { - "type": "string" - }, - { - "properties": { - "calls": { - "maximum": 18446744073709551615, - "minimum": 0, - "type": "integer" - }, - "errors": { - "maximum": 18446744073709551615, - "minimum": 0, - "type": "integer" - }, - "failures": { - "maximum": 18446744073709551615, - "minimum": 0, - "type": "integer" - } - }, - "required": [ - "calls", - "errors", - "failures" - ], - "type": "object" - } - ], - "type": "array" + "additionalProperties": { + "additionalProperties": { + "properties": { + "calls": { + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + }, + "errors": { + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" }, - "type": "array" - } - ], - "type": "array" + "failures": { + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "calls", + "errors", + "failures" + ], + "type": "object" + }, + "type": "object" }, - "type": "array" + "type": "object" } }, "required": [ diff --git a/doc/schemas/gov_openapi.json b/doc/schemas/gov_openapi.json new file mode 100644 index 00000000000..99c42226427 --- /dev/null +++ b/doc/schemas/gov_openapi.json @@ -0,0 +1,1211 @@ +{ + "components": { + "schemas": { + "CallerInfo": { + "properties": { + "caller_id": { + "$ref": "#/components/schemas/uint64" + } + }, + "required": [ + "caller_id" + ], + "type": "object" + }, + "CodeStatus": { + "enum": [ + "ACCEPTED", + "RETIRED" + ] + }, + "EndpointMetrics__Metric": { + "properties": { + "calls": { + "$ref": "#/components/schemas/uint64" + }, + "errors": { + "$ref": "#/components/schemas/uint64" + }, + "failures": { + "$ref": "#/components/schemas/uint64" + } + }, + "required": [ + "calls", + "errors", + "failures" + ], + "type": "object" + }, + "EndpointMetrics__Out": { + "properties": { + "metrics": { + "$ref": "#/components/schemas/named_named_EndpointMetrics__Metric" + } + }, + "required": [ + "metrics" + ], + "type": "object" + }, + "GetCode__Out": { + "properties": { + "versions": { + "$ref": "#/components/schemas/GetCode__Version_array" + } + }, + "required": [ + "versions" + ], + "type": "object" + }, + "GetCode__Version": { + "properties": { + "digest": { + "$ref": "#/components/schemas/string" + }, + "status": { + "$ref": "#/components/schemas/CodeStatus" + } + }, + "required": [ + "digest", + "status" + ], + "type": "object" + }, + "GetCode__Version_array": { + "items": { + "$ref": "#/components/schemas/GetCode__Version" + }, + "type": "array" + }, + "GetCommit__Out": { + "properties": { + "seqno": { + "$ref": "#/components/schemas/int64" + }, + "view": { + "$ref": "#/components/schemas/int64" + } + }, + "required": [ + "view", + "seqno" + ], + "type": "object" + }, + "GetEncryptedRecoveryShare": { + "properties": { + "encrypted_recovery_share": { + "$ref": "#/components/schemas/string" + }, + "nonce": { + "$ref": "#/components/schemas/string" + } + }, + "required": [ + "encrypted_recovery_share", + "nonce" + ], + "type": "object" + }, + "GetMetrics__HistogramResults": { + "properties": { + "buckets": { + "$ref": "#/components/schemas/json" + }, + "high": { + "$ref": "#/components/schemas/int32" + }, + "low": { + "$ref": "#/components/schemas/int32" + }, + "overflow": { + "$ref": "#/components/schemas/uint64" + }, + "underflow": { + "$ref": "#/components/schemas/uint64" + } + }, + "required": [ + "low", + "high", + "overflow", + "underflow", + "buckets" + ], + "type": "object" + }, + "GetMetrics__Out": { + "properties": { + "histogram": { + "$ref": "#/components/schemas/GetMetrics__HistogramResults" + }, + "tx_rates": { + "$ref": "#/components/schemas/json" + } + }, + "required": [ + "histogram", + "tx_rates" + ], + "type": "object" + }, + "GetNetworkInfo__NodeInfo": { + "properties": { + "host": { + "$ref": "#/components/schemas/string" + }, + "node_id": { + "$ref": "#/components/schemas/uint64" + }, + "port": { + "$ref": "#/components/schemas/string" + } + }, + "required": [ + "node_id", + "host", + "port" + ], + "type": "object" + }, + "GetNetworkInfo__NodeInfo_array": { + "items": { + "$ref": "#/components/schemas/GetNetworkInfo__NodeInfo" + }, + "type": "array" + }, + "GetNetworkInfo__Out": { + "properties": { + "nodes": { + "$ref": "#/components/schemas/GetNetworkInfo__NodeInfo_array" + }, + "primary_id": { + "$ref": "#/components/schemas/uint64" + } + }, + "required": [ + "nodes", + "primary_id" + ], + "type": "object" + }, + "GetNodesByRPCAddress__NodeInfo": { + "properties": { + "node_id": { + "$ref": "#/components/schemas/uint64" + }, + "status": { + "$ref": "#/components/schemas/NodeStatus" + } + }, + "required": [ + "node_id", + "status" + ], + "type": "object" + }, + "GetNodesByRPCAddress__NodeInfo_array": { + "items": { + "$ref": "#/components/schemas/GetNodesByRPCAddress__NodeInfo" + }, + "type": "array" + }, + "GetNodesByRPCAddress__Out": { + "properties": { + "nodes": { + "$ref": "#/components/schemas/GetNodesByRPCAddress__NodeInfo_array" + } + }, + "required": [ + "nodes" + ], + "type": "object" + }, + "GetPrimaryInfo__Out": { + "properties": { + "current_view": { + "$ref": "#/components/schemas/int64" + }, + "primary_host": { + "$ref": "#/components/schemas/string" + }, + "primary_id": { + "$ref": "#/components/schemas/uint64" + }, + "primary_port": { + "$ref": "#/components/schemas/string" + } + }, + "required": [ + "primary_id", + "primary_host", + "primary_port", + "current_view" + ], + "type": "object" + }, + "GetReceipt__Out": { + "properties": { + "receipt": { + "$ref": "#/components/schemas/uint8_array" + } + }, + "required": [ + "receipt" + ], + "type": "object" + }, + "GetSchema__Out": { + "properties": { + "params_schema": { + "$ref": "#/components/schemas/json_schema" + }, + "result_schema": { + "$ref": "#/components/schemas/json_schema" + } + }, + "required": [ + "params_schema", + "result_schema" + ], + "type": "object" + }, + "GetTxStatus__Out": { + "properties": { + "status": { + "$ref": "#/components/schemas/TxStatus" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "KVRead__In": { + "properties": { + "key": { + "$ref": "#/components/schemas/json" + }, + "table": { + "$ref": "#/components/schemas/string" + } + }, + "required": [ + "table", + "key" + ], + "type": "object" + }, + "MemberAck": { + "properties": { + "signed_req": { + "$ref": "#/components/schemas/SignedReq" + }, + "state_digest": { + "$ref": "#/components/schemas/uint8_array" + } + }, + "required": [ + "state_digest", + "signed_req" + ], + "type": "object" + }, + "NodeStatus": { + "enum": [ + "PENDING", + "TRUSTED", + "RETIRED" + ] + }, + "Proposal": { + "properties": { + "parameter": { + "$ref": "#/components/schemas/json" + }, + "proposer": { + "$ref": "#/components/schemas/uint64" + }, + "script": { + "$ref": "#/components/schemas/Script" + }, + "state": { + "$ref": "#/components/schemas/ProposalState" + }, + "votes": { + "$ref": "#/components/schemas/uint64_to_Script" + } + }, + "required": [ + "script", + "parameter", + "proposer", + "state", + "votes" + ], + "type": "object" + }, + "ProposalInfo": { + "properties": { + "proposal_id": { + "$ref": "#/components/schemas/uint64" + }, + "proposer_id": { + "$ref": "#/components/schemas/uint64" + }, + "state": { + "$ref": "#/components/schemas/ProposalState" + } + }, + "required": [ + "proposal_id", + "proposer_id", + "state" + ], + "type": "object" + }, + "ProposalState": { + "enum": [ + "OPEN", + "ACCEPTED", + "WITHDRAWN", + "REJECTED", + "FAILED" + ] + }, + "Propose__In": { + "properties": { + "ballot": { + "$ref": "#/components/schemas/Script" + }, + "parameter": { + "$ref": "#/components/schemas/json" + }, + "script": { + "$ref": "#/components/schemas/Script" + } + }, + "required": [ + "script" + ], + "type": "object" + }, + "Script": { + "properties": { + "bytecode": { + "$ref": "#/components/schemas/uint8_array" + }, + "text": { + "$ref": "#/components/schemas/string" + } + }, + "type": "object" + }, + "SignedReq": { + "properties": { + "md": { + "$ref": "#/components/schemas/mbedtls_md_type_t" + }, + "req": { + "$ref": "#/components/schemas/uint8_array" + }, + "request_body": { + "$ref": "#/components/schemas/uint8_array" + }, + "sig": { + "$ref": "#/components/schemas/uint8_array" + } + }, + "required": [ + "sig", + "req", + "request_body", + "md" + ], + "type": "object" + }, + "StateDigest": { + "properties": { + "state_digest": { + "$ref": "#/components/schemas/uint8_array" + } + }, + "required": [ + "state_digest" + ], + "type": "object" + }, + "TxStatus": { + "enum": [ + "UNKNOWN", + "PENDING", + "COMMITTED", + "INVALID" + ] + }, + "VerifyReceipt__In": { + "properties": { + "receipt": { + "$ref": "#/components/schemas/uint8_array" + } + }, + "required": [ + "receipt" + ], + "type": "object" + }, + "VerifyReceipt__Out": { + "properties": { + "valid": { + "$ref": "#/components/schemas/boolean" + } + }, + "required": [ + "valid" + ], + "type": "object" + }, + "Vote": { + "properties": { + "ballot": { + "$ref": "#/components/schemas/Script" + } + }, + "required": [ + "ballot" + ], + "type": "object" + }, + "boolean": { + "type": "boolean" + }, + "int32": { + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "int64": { + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + }, + "json": {}, + "json_schema": { + "type": "object" + }, + "mbedtls_md_type_t": { + "enum": [ + "MBEDTLS_MD_NONE", + "MBEDTLS_MD_SHA1", + "MBEDTLS_MD_SHA256", + "MBEDTLS_MD_SHA384", + "MBEDTLS_MD_SHA512" + ] + }, + "named_EndpointMetrics__Metric": { + "additionalProperties": { + "$ref": "#/components/schemas/EndpointMetrics__Metric" + }, + "type": "object" + }, + "named_named_EndpointMetrics__Metric": { + "additionalProperties": { + "$ref": "#/components/schemas/named_EndpointMetrics__Metric" + }, + "type": "object" + }, + "string": { + "type": "string" + }, + "uint64": { + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + }, + "uint64_to_Script": { + "items": { + "items": [ + { + "$ref": "#/components/schemas/uint64" + }, + { + "$ref": "#/components/schemas/Script" + } + ], + "type": "array" + }, + "type": "array" + }, + "uint8": { + "maximum": 255, + "minimum": 0, + "type": "integer" + }, + "uint8_array": { + "items": { + "$ref": "#/components/schemas/uint8" + }, + "type": "array" + } + } + }, + "info": { + "description": "This API is used to submit and query proposals which affect CCF's public governance tables.", + "title": "CCF Governance API", + "version": "0.0.1" + }, + "openapi": "3.0.0", + "paths": { + "/ack": { + "post": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StateDigest" + } + } + }, + "description": "Auto-generated request body schema" + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/boolean" + } + } + }, + "description": "Default response description" + } + } + } + }, + "/ack/update_state_digest": { + "post": { + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MemberAck" + } + } + }, + "description": "Default response description" + } + } + } + }, + "/api": { + "get": { + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/json" + } + } + }, + "description": "Default response description" + } + } + } + }, + "/api/schema": { + "get": { + "parameters": [ + { + "in": "query", + "name": "method", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetSchema__Out" + } + } + }, + "description": "Default response description" + } + } + } + }, + "/code": { + "get": { + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetCode__Out" + } + } + }, + "description": "Default response description" + } + } + } + }, + "/commit": { + "get": { + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetCommit__Out" + } + } + }, + "description": "Default response description" + } + } + } + }, + "/endpoint_metrics": { + "get": { + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EndpointMetrics__Out" + } + } + }, + "description": "Default response description" + } + } + } + }, + "/local_tx": { + "get": { + "parameters": [ + { + "in": "query", + "name": "seqno", + "required": false, + "schema": { + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + } + }, + { + "in": "query", + "name": "view", + "required": false, + "schema": { + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetTxStatus__Out" + } + } + }, + "description": "Default response description" + } + } + } + }, + "/metrics": { + "get": { + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetMetrics__Out" + } + } + }, + "description": "Default response description" + } + } + } + }, + "/network_info": { + "get": { + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetNetworkInfo__Out" + } + } + }, + "description": "Default response description" + } + } + } + }, + "/node/ids": { + "get": { + "parameters": [ + { + "in": "query", + "name": "host", + "required": false, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "port", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetNodesByRPCAddress__Out" + } + } + }, + "description": "Default response description" + } + } + } + }, + "/primary_info": { + "get": { + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetPrimaryInfo__Out" + } + } + }, + "description": "Default response description" + } + } + } + }, + "/proposals": { + "post": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Propose__In" + } + } + }, + "description": "Auto-generated request body schema" + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProposalInfo" + } + } + }, + "description": "Default response description" + } + } + } + }, + "/proposals/{proposal_id}": { + "get": { + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Proposal" + } + } + }, + "description": "Default response description" + } + } + }, + "parameters": [ + { + "in": "path", + "name": "proposal_id", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/proposals/{proposal_id}/complete": { + "parameters": [ + { + "in": "path", + "name": "proposal_id", + "required": true, + "schema": { + "type": "string" + } + } + ], + "post": { + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProposalInfo" + } + } + }, + "description": "Default response description" + } + } + } + }, + "/proposals/{proposal_id}/votes": { + "parameters": [ + { + "in": "path", + "name": "proposal_id", + "required": true, + "schema": { + "type": "string" + } + } + ], + "post": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Vote" + } + } + }, + "description": "Auto-generated request body schema" + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProposalInfo" + } + } + }, + "description": "Default response description" + } + } + } + }, + "/proposals/{proposal_id}/votes/{member_id}": { + "get": { + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Vote" + } + } + }, + "description": "Default response description" + } + } + }, + "parameters": [ + { + "in": "path", + "name": "proposal_id", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "member_id", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/proposals/{proposal_id}/withdraw": { + "parameters": [ + { + "in": "path", + "name": "proposal_id", + "required": true, + "schema": { + "type": "string" + } + } + ], + "post": { + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProposalInfo" + } + } + }, + "description": "Default response description" + } + } + } + }, + "/query": { + "post": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Script" + } + } + }, + "description": "Auto-generated request body schema" + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/json" + } + } + }, + "description": "Default response description" + } + } + } + }, + "/read": { + "post": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/KVRead__In" + } + } + }, + "description": "Auto-generated request body schema" + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/json" + } + } + }, + "description": "Default response description" + } + } + } + }, + "/receipt": { + "get": { + "parameters": [ + { + "in": "query", + "name": "commit", + "required": false, + "schema": { + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetReceipt__Out" + } + } + }, + "description": "Default response description" + } + } + } + }, + "/receipt/verify": { + "post": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VerifyReceipt__In" + } + } + }, + "description": "Auto-generated request body schema" + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VerifyReceipt__Out" + } + } + }, + "description": "Default response description" + } + } + } + }, + "/recovery_share": { + "get": { + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetEncryptedRecoveryShare" + } + } + }, + "description": "Default response description" + } + } + } + }, + "/recovery_share/submit": { + "post": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/string" + } + } + }, + "description": "Auto-generated request body schema" + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/string" + } + } + }, + "description": "Default response description" + } + } + } + }, + "/tx": { + "get": { + "parameters": [ + { + "in": "query", + "name": "seqno", + "required": false, + "schema": { + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + } + }, + { + "in": "query", + "name": "view", + "required": false, + "schema": { + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetTxStatus__Out" + } + } + }, + "description": "Default response description" + } + } + } + }, + "/user_id": { + "get": { + "parameters": [ + { + "in": "query", + "name": "cert", + "required": false, + "schema": { + "items": { + "maximum": 255, + "minimum": 0, + "type": "integer" + }, + "type": "array" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CallerInfo" + } + } + }, + "description": "Default response description" + } + } + } + } + }, + "servers": [ + { + "url": "/gov" + } + ] +} \ No newline at end of file diff --git a/doc/schemas/local_tx_GET_params.json b/doc/schemas/local_tx_GET_params.json index 2f5c752fc70..1d62fc03e13 100644 --- a/doc/schemas/local_tx_GET_params.json +++ b/doc/schemas/local_tx_GET_params.json @@ -1,5 +1,4 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", "properties": { "seqno": { "maximum": 9223372036854775807, diff --git a/doc/schemas/local_tx_GET_result.json b/doc/schemas/local_tx_GET_result.json index 70f76814f8e..beec85746a5 100644 --- a/doc/schemas/local_tx_GET_result.json +++ b/doc/schemas/local_tx_GET_result.json @@ -1,5 +1,4 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", "properties": { "status": { "enum": [ diff --git a/doc/schemas/log/private/admin_only_POST_params.json b/doc/schemas/log/private/admin_only_POST_params.json index ceaabad8d66..d2fa79cb085 100644 --- a/doc/schemas/log/private/admin_only_POST_params.json +++ b/doc/schemas/log/private/admin_only_POST_params.json @@ -1,5 +1,4 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", "properties": { "id": { "maximum": 18446744073709551615, diff --git a/doc/schemas/log/private/admin_only_POST_result.json b/doc/schemas/log/private/admin_only_POST_result.json index fcd126c2653..f731d3ec9a6 100644 --- a/doc/schemas/log/private/admin_only_POST_result.json +++ b/doc/schemas/log/private/admin_only_POST_result.json @@ -1,5 +1,4 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", "title": "log/private/admin_only/result", "type": "boolean" } \ No newline at end of file diff --git a/doc/schemas/log/private/anonymous_POST_params.json b/doc/schemas/log/private/anonymous_POST_params.json index 8804ee0b1cb..ed540feab5b 100644 --- a/doc/schemas/log/private/anonymous_POST_params.json +++ b/doc/schemas/log/private/anonymous_POST_params.json @@ -1,5 +1,4 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", "properties": { "id": { "maximum": 18446744073709551615, diff --git a/doc/schemas/log/private/anonymous_POST_result.json b/doc/schemas/log/private/anonymous_POST_result.json index 90b546a12e0..d78e3311f84 100644 --- a/doc/schemas/log/private/anonymous_POST_result.json +++ b/doc/schemas/log/private/anonymous_POST_result.json @@ -1,5 +1,4 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", "title": "log/private/anonymous/result", "type": "boolean" } \ No newline at end of file diff --git a/doc/schemas/log/private_DELETE_params.json b/doc/schemas/log/private_DELETE_params.json index d0c68127024..59f5f00cd71 100644 --- a/doc/schemas/log/private_DELETE_params.json +++ b/doc/schemas/log/private_DELETE_params.json @@ -1,5 +1,4 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", "properties": { "id": { "maximum": 18446744073709551615, diff --git a/doc/schemas/log/private_DELETE_result.json b/doc/schemas/log/private_DELETE_result.json index 71f54370042..423e1d35077 100644 --- a/doc/schemas/log/private_DELETE_result.json +++ b/doc/schemas/log/private_DELETE_result.json @@ -1,5 +1,4 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", "title": "log/private/result", "type": "boolean" } \ No newline at end of file diff --git a/doc/schemas/log/private_GET_params.json b/doc/schemas/log/private_GET_params.json index d0c68127024..59f5f00cd71 100644 --- a/doc/schemas/log/private_GET_params.json +++ b/doc/schemas/log/private_GET_params.json @@ -1,5 +1,4 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", "properties": { "id": { "maximum": 18446744073709551615, diff --git a/doc/schemas/log/private_GET_result.json b/doc/schemas/log/private_GET_result.json index 69d63f77b21..67c8963ac93 100644 --- a/doc/schemas/log/private_GET_result.json +++ b/doc/schemas/log/private_GET_result.json @@ -1,5 +1,4 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", "properties": { "msg": { "type": "string" diff --git a/doc/schemas/log/private_POST_params.json b/doc/schemas/log/private_POST_params.json index e26f8a7fcc1..3bf318bade9 100644 --- a/doc/schemas/log/private_POST_params.json +++ b/doc/schemas/log/private_POST_params.json @@ -1,5 +1,4 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", "properties": { "id": { "maximum": 18446744073709551615, diff --git a/doc/schemas/log/private_POST_result.json b/doc/schemas/log/private_POST_result.json index 71f54370042..423e1d35077 100644 --- a/doc/schemas/log/private_POST_result.json +++ b/doc/schemas/log/private_POST_result.json @@ -1,5 +1,4 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", "title": "log/private/result", "type": "boolean" } \ No newline at end of file diff --git a/doc/schemas/log/private_WEBSOCKET_params.json b/doc/schemas/log/private_WEBSOCKET_params.json index e26f8a7fcc1..3bf318bade9 100644 --- a/doc/schemas/log/private_WEBSOCKET_params.json +++ b/doc/schemas/log/private_WEBSOCKET_params.json @@ -1,5 +1,4 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", "properties": { "id": { "maximum": 18446744073709551615, diff --git a/doc/schemas/log/private_WEBSOCKET_result.json b/doc/schemas/log/private_WEBSOCKET_result.json index 71f54370042..423e1d35077 100644 --- a/doc/schemas/log/private_WEBSOCKET_result.json +++ b/doc/schemas/log/private_WEBSOCKET_result.json @@ -1,5 +1,4 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", "title": "log/private/result", "type": "boolean" } \ No newline at end of file diff --git a/doc/schemas/log/public_DELETE_params.json b/doc/schemas/log/public_DELETE_params.json index 98ee62d9f25..0dcbe93a651 100644 --- a/doc/schemas/log/public_DELETE_params.json +++ b/doc/schemas/log/public_DELETE_params.json @@ -1,5 +1,4 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", "properties": { "id": { "maximum": 18446744073709551615, diff --git a/doc/schemas/log/public_DELETE_result.json b/doc/schemas/log/public_DELETE_result.json index e0f82e22e8f..213d0cb22de 100644 --- a/doc/schemas/log/public_DELETE_result.json +++ b/doc/schemas/log/public_DELETE_result.json @@ -1,5 +1,4 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", "title": "log/public/result", "type": "boolean" } \ No newline at end of file diff --git a/doc/schemas/log/public_GET_params.json b/doc/schemas/log/public_GET_params.json index d5ba681836e..9a85d305432 100644 --- a/doc/schemas/log/public_GET_params.json +++ b/doc/schemas/log/public_GET_params.json @@ -1,5 +1,4 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", "properties": { "id": { "type": "number" diff --git a/doc/schemas/log/public_GET_result.json b/doc/schemas/log/public_GET_result.json index f835f4c47fd..1f430b8ec83 100644 --- a/doc/schemas/log/public_GET_result.json +++ b/doc/schemas/log/public_GET_result.json @@ -1,5 +1,4 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", "properties": { "msg": { "type": "string" diff --git a/doc/schemas/log/public_POST_params.json b/doc/schemas/log/public_POST_params.json index 90a9d7c15db..6bd2c399a6e 100644 --- a/doc/schemas/log/public_POST_params.json +++ b/doc/schemas/log/public_POST_params.json @@ -1,5 +1,4 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", "properties": { "id": { "type": "number" diff --git a/doc/schemas/log/public_POST_result.json b/doc/schemas/log/public_POST_result.json index 4aa3c762269..213d0cb22de 100644 --- a/doc/schemas/log/public_POST_result.json +++ b/doc/schemas/log/public_POST_result.json @@ -1,5 +1,4 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", "title": "log/public/result", - "type": "bool" + "type": "boolean" } \ No newline at end of file diff --git a/doc/schemas/metrics_GET_result.json b/doc/schemas/metrics_GET_result.json index 9ea8c00bd85..2c5918392dd 100644 --- a/doc/schemas/metrics_GET_result.json +++ b/doc/schemas/metrics_GET_result.json @@ -1,5 +1,4 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", "properties": { "histogram": { "properties": { diff --git a/doc/schemas/network_info_GET_result.json b/doc/schemas/network_info_GET_result.json index aff7664a939..eb2cb3c1902 100644 --- a/doc/schemas/network_info_GET_result.json +++ b/doc/schemas/network_info_GET_result.json @@ -1,5 +1,4 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", "properties": { "nodes": { "items": { diff --git a/doc/schemas/node/ids_GET_params.json b/doc/schemas/node/ids_GET_params.json index c5e859a8264..aa5aa48df91 100644 --- a/doc/schemas/node/ids_GET_params.json +++ b/doc/schemas/node/ids_GET_params.json @@ -1,5 +1,4 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", "properties": { "host": { "type": "string" diff --git a/doc/schemas/node/ids_GET_result.json b/doc/schemas/node/ids_GET_result.json index e4728dbba13..de4e8bbb826 100644 --- a/doc/schemas/node/ids_GET_result.json +++ b/doc/schemas/node/ids_GET_result.json @@ -1,5 +1,4 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", "properties": { "nodes": { "items": { diff --git a/doc/schemas/node_openapi.json b/doc/schemas/node_openapi.json new file mode 100644 index 00000000000..84625b1723b --- /dev/null +++ b/doc/schemas/node_openapi.json @@ -0,0 +1,785 @@ +{ + "components": { + "schemas": { + "CodeStatus": { + "enum": [ + "ACCEPTED", + "RETIRED" + ] + }, + "EndpointMetrics__Metric": { + "properties": { + "calls": { + "$ref": "#/components/schemas/uint64" + }, + "errors": { + "$ref": "#/components/schemas/uint64" + }, + "failures": { + "$ref": "#/components/schemas/uint64" + } + }, + "required": [ + "calls", + "errors", + "failures" + ], + "type": "object" + }, + "EndpointMetrics__Out": { + "properties": { + "metrics": { + "$ref": "#/components/schemas/named_named_EndpointMetrics__Metric" + } + }, + "required": [ + "metrics" + ], + "type": "object" + }, + "GetCode__Out": { + "properties": { + "versions": { + "$ref": "#/components/schemas/GetCode__Version_array" + } + }, + "required": [ + "versions" + ], + "type": "object" + }, + "GetCode__Version": { + "properties": { + "digest": { + "$ref": "#/components/schemas/string" + }, + "status": { + "$ref": "#/components/schemas/CodeStatus" + } + }, + "required": [ + "digest", + "status" + ], + "type": "object" + }, + "GetCode__Version_array": { + "items": { + "$ref": "#/components/schemas/GetCode__Version" + }, + "type": "array" + }, + "GetCommit__Out": { + "properties": { + "seqno": { + "$ref": "#/components/schemas/int64" + }, + "view": { + "$ref": "#/components/schemas/int64" + } + }, + "required": [ + "view", + "seqno" + ], + "type": "object" + }, + "GetMetrics__HistogramResults": { + "properties": { + "buckets": { + "$ref": "#/components/schemas/json" + }, + "high": { + "$ref": "#/components/schemas/int32" + }, + "low": { + "$ref": "#/components/schemas/int32" + }, + "overflow": { + "$ref": "#/components/schemas/uint64" + }, + "underflow": { + "$ref": "#/components/schemas/uint64" + } + }, + "required": [ + "low", + "high", + "overflow", + "underflow", + "buckets" + ], + "type": "object" + }, + "GetMetrics__Out": { + "properties": { + "histogram": { + "$ref": "#/components/schemas/GetMetrics__HistogramResults" + }, + "tx_rates": { + "$ref": "#/components/schemas/json" + } + }, + "required": [ + "histogram", + "tx_rates" + ], + "type": "object" + }, + "GetNetworkInfo__NodeInfo": { + "properties": { + "host": { + "$ref": "#/components/schemas/string" + }, + "node_id": { + "$ref": "#/components/schemas/uint64" + }, + "port": { + "$ref": "#/components/schemas/string" + } + }, + "required": [ + "node_id", + "host", + "port" + ], + "type": "object" + }, + "GetNetworkInfo__NodeInfo_array": { + "items": { + "$ref": "#/components/schemas/GetNetworkInfo__NodeInfo" + }, + "type": "array" + }, + "GetNetworkInfo__Out": { + "properties": { + "nodes": { + "$ref": "#/components/schemas/GetNetworkInfo__NodeInfo_array" + }, + "primary_id": { + "$ref": "#/components/schemas/uint64" + } + }, + "required": [ + "nodes", + "primary_id" + ], + "type": "object" + }, + "GetNodesByRPCAddress__NodeInfo": { + "properties": { + "node_id": { + "$ref": "#/components/schemas/uint64" + }, + "status": { + "$ref": "#/components/schemas/NodeStatus" + } + }, + "required": [ + "node_id", + "status" + ], + "type": "object" + }, + "GetNodesByRPCAddress__NodeInfo_array": { + "items": { + "$ref": "#/components/schemas/GetNodesByRPCAddress__NodeInfo" + }, + "type": "array" + }, + "GetNodesByRPCAddress__Out": { + "properties": { + "nodes": { + "$ref": "#/components/schemas/GetNodesByRPCAddress__NodeInfo_array" + } + }, + "required": [ + "nodes" + ], + "type": "object" + }, + "GetPrimaryInfo__Out": { + "properties": { + "current_view": { + "$ref": "#/components/schemas/int64" + }, + "primary_host": { + "$ref": "#/components/schemas/string" + }, + "primary_id": { + "$ref": "#/components/schemas/uint64" + }, + "primary_port": { + "$ref": "#/components/schemas/string" + } + }, + "required": [ + "primary_id", + "primary_host", + "primary_port", + "current_view" + ], + "type": "object" + }, + "GetQuotes__Out": { + "properties": { + "quotes": { + "$ref": "#/components/schemas/GetQuotes__Quote_array" + } + }, + "required": [ + "quotes" + ], + "type": "object" + }, + "GetQuotes__Quote": { + "properties": { + "error": { + "$ref": "#/components/schemas/string" + }, + "mrenclave": { + "$ref": "#/components/schemas/string" + }, + "node_id": { + "$ref": "#/components/schemas/uint64" + }, + "raw": { + "$ref": "#/components/schemas/string" + } + }, + "required": [ + "node_id", + "raw" + ], + "type": "object" + }, + "GetQuotes__Quote_array": { + "items": { + "$ref": "#/components/schemas/GetQuotes__Quote" + }, + "type": "array" + }, + "GetReceipt__Out": { + "properties": { + "receipt": { + "$ref": "#/components/schemas/uint8_array" + } + }, + "required": [ + "receipt" + ], + "type": "object" + }, + "GetSchema__Out": { + "properties": { + "params_schema": { + "$ref": "#/components/schemas/json_schema" + }, + "result_schema": { + "$ref": "#/components/schemas/json_schema" + } + }, + "required": [ + "params_schema", + "result_schema" + ], + "type": "object" + }, + "GetState__Out": { + "properties": { + "id": { + "$ref": "#/components/schemas/uint64" + }, + "last_recovered_seqno": { + "$ref": "#/components/schemas/int64" + }, + "last_signed_seqno": { + "$ref": "#/components/schemas/int64" + }, + "recovery_target_seqno": { + "$ref": "#/components/schemas/int64" + }, + "state": { + "$ref": "#/components/schemas/ccf__State" + } + }, + "required": [ + "id", + "state", + "last_signed_seqno" + ], + "type": "object" + }, + "GetTxStatus__Out": { + "properties": { + "status": { + "$ref": "#/components/schemas/TxStatus" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "NodeStatus": { + "enum": [ + "PENDING", + "TRUSTED", + "RETIRED" + ] + }, + "TxStatus": { + "enum": [ + "UNKNOWN", + "PENDING", + "COMMITTED", + "INVALID" + ] + }, + "VerifyReceipt__In": { + "properties": { + "receipt": { + "$ref": "#/components/schemas/uint8_array" + } + }, + "required": [ + "receipt" + ], + "type": "object" + }, + "VerifyReceipt__Out": { + "properties": { + "valid": { + "$ref": "#/components/schemas/boolean" + } + }, + "required": [ + "valid" + ], + "type": "object" + }, + "boolean": { + "type": "boolean" + }, + "ccf__State": { + "enum": [ + "uninitialized", + "initialized", + "pending", + "partOfPublicNetwork", + "partOfNetwork", + "readingPublicLedger", + "readingPrivateLedger" + ] + }, + "int32": { + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "int64": { + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + }, + "json": {}, + "json_schema": { + "type": "object" + }, + "named_EndpointMetrics__Metric": { + "additionalProperties": { + "$ref": "#/components/schemas/EndpointMetrics__Metric" + }, + "type": "object" + }, + "named_named_EndpointMetrics__Metric": { + "additionalProperties": { + "$ref": "#/components/schemas/named_EndpointMetrics__Metric" + }, + "type": "object" + }, + "string": { + "type": "string" + }, + "uint64": { + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + }, + "uint8": { + "maximum": 255, + "minimum": 0, + "type": "integer" + }, + "uint8_array": { + "items": { + "$ref": "#/components/schemas/uint8" + }, + "type": "array" + } + } + }, + "info": { + "description": "This API provides public, uncredentialed access to service and node state.", + "title": "CCF Public Node API", + "version": "0.0.1" + }, + "openapi": "3.0.0", + "paths": { + "/api": { + "get": { + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/json" + } + } + }, + "description": "Default response description" + } + } + } + }, + "/api/schema": { + "get": { + "parameters": [ + { + "in": "query", + "name": "method", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetSchema__Out" + } + } + }, + "description": "Default response description" + } + } + } + }, + "/code": { + "get": { + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetCode__Out" + } + } + }, + "description": "Default response description" + } + } + } + }, + "/commit": { + "get": { + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetCommit__Out" + } + } + }, + "description": "Default response description" + } + } + } + }, + "/endpoint_metrics": { + "get": { + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EndpointMetrics__Out" + } + } + }, + "description": "Default response description" + } + } + } + }, + "/local_tx": { + "get": { + "parameters": [ + { + "in": "query", + "name": "seqno", + "required": false, + "schema": { + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + } + }, + { + "in": "query", + "name": "view", + "required": false, + "schema": { + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetTxStatus__Out" + } + } + }, + "description": "Default response description" + } + } + } + }, + "/metrics": { + "get": { + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetMetrics__Out" + } + } + }, + "description": "Default response description" + } + } + } + }, + "/network_info": { + "get": { + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetNetworkInfo__Out" + } + } + }, + "description": "Default response description" + } + } + } + }, + "/node/ids": { + "get": { + "parameters": [ + { + "in": "query", + "name": "host", + "required": false, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "port", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetNodesByRPCAddress__Out" + } + } + }, + "description": "Default response description" + } + } + } + }, + "/primary_info": { + "get": { + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetPrimaryInfo__Out" + } + } + }, + "description": "Default response description" + } + } + } + }, + "/quote": { + "get": { + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetQuotes__Out" + } + } + }, + "description": "Default response description" + } + } + } + }, + "/quotes": { + "get": { + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetQuotes__Out" + } + } + }, + "description": "Default response description" + } + } + } + }, + "/receipt": { + "get": { + "parameters": [ + { + "in": "query", + "name": "commit", + "required": false, + "schema": { + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetReceipt__Out" + } + } + }, + "description": "Default response description" + } + } + } + }, + "/receipt/verify": { + "post": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VerifyReceipt__In" + } + } + }, + "description": "Auto-generated request body schema" + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VerifyReceipt__Out" + } + } + }, + "description": "Default response description" + } + } + } + }, + "/state": { + "get": { + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetState__Out" + } + } + }, + "description": "Default response description" + } + } + } + }, + "/tx": { + "get": { + "parameters": [ + { + "in": "query", + "name": "seqno", + "required": false, + "schema": { + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + } + }, + { + "in": "query", + "name": "view", + "required": false, + "schema": { + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetTxStatus__Out" + } + } + }, + "description": "Default response description" + } + } + } + } + }, + "servers": [ + { + "url": "/node" + } + ] +} \ No newline at end of file diff --git a/doc/schemas/primary_info_GET_result.json b/doc/schemas/primary_info_GET_result.json index a326a9e9b93..43e11dbb049 100644 --- a/doc/schemas/primary_info_GET_result.json +++ b/doc/schemas/primary_info_GET_result.json @@ -1,5 +1,4 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", "properties": { "current_view": { "maximum": 9223372036854775807, diff --git a/doc/schemas/proposals/{proposal_id}/complete_POST_result.json b/doc/schemas/proposals/{proposal_id}/complete_POST_result.json index 6280ae350bd..6287c75c763 100644 --- a/doc/schemas/proposals/{proposal_id}/complete_POST_result.json +++ b/doc/schemas/proposals/{proposal_id}/complete_POST_result.json @@ -1,5 +1,4 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", "properties": { "proposal_id": { "maximum": 18446744073709551615, diff --git a/doc/schemas/proposals/{proposal_id}/votes/{member_id}_GET_result.json b/doc/schemas/proposals/{proposal_id}/votes/{member_id}_GET_result.json index 2009e1654af..5b970d9f4ef 100644 --- a/doc/schemas/proposals/{proposal_id}/votes/{member_id}_GET_result.json +++ b/doc/schemas/proposals/{proposal_id}/votes/{member_id}_GET_result.json @@ -1,5 +1,4 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", "properties": { "ballot": { "properties": { diff --git a/doc/schemas/proposals/{proposal_id}/votes_POST_params.json b/doc/schemas/proposals/{proposal_id}/votes_POST_params.json index 71b98f00e15..ec8d8edae48 100644 --- a/doc/schemas/proposals/{proposal_id}/votes_POST_params.json +++ b/doc/schemas/proposals/{proposal_id}/votes_POST_params.json @@ -1,5 +1,4 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", "properties": { "ballot": { "properties": { diff --git a/doc/schemas/proposals/{proposal_id}/votes_POST_result.json b/doc/schemas/proposals/{proposal_id}/votes_POST_result.json index 18f4d4df99f..f052c8c35d1 100644 --- a/doc/schemas/proposals/{proposal_id}/votes_POST_result.json +++ b/doc/schemas/proposals/{proposal_id}/votes_POST_result.json @@ -1,5 +1,4 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", "properties": { "proposal_id": { "maximum": 18446744073709551615, diff --git a/doc/schemas/proposals/{proposal_id}/withdraw_POST_result.json b/doc/schemas/proposals/{proposal_id}/withdraw_POST_result.json index 3bb507fc4c1..2a38ef2fff8 100644 --- a/doc/schemas/proposals/{proposal_id}/withdraw_POST_result.json +++ b/doc/schemas/proposals/{proposal_id}/withdraw_POST_result.json @@ -1,5 +1,4 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", "properties": { "proposal_id": { "maximum": 18446744073709551615, diff --git a/doc/schemas/proposals/{proposal_id}_GET_result.json b/doc/schemas/proposals/{proposal_id}_GET_result.json index d730343b949..48d91ddab99 100644 --- a/doc/schemas/proposals/{proposal_id}_GET_result.json +++ b/doc/schemas/proposals/{proposal_id}_GET_result.json @@ -1,5 +1,4 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", "properties": { "parameter": {}, "proposer": { diff --git a/doc/schemas/proposals_POST_params.json b/doc/schemas/proposals_POST_params.json index 1afce0680a5..6708dc01b63 100644 --- a/doc/schemas/proposals_POST_params.json +++ b/doc/schemas/proposals_POST_params.json @@ -1,5 +1,4 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", "properties": { "ballot": { "properties": { diff --git a/doc/schemas/proposals_POST_result.json b/doc/schemas/proposals_POST_result.json index a68771ea0f8..f33004c21b5 100644 --- a/doc/schemas/proposals_POST_result.json +++ b/doc/schemas/proposals_POST_result.json @@ -1,5 +1,4 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", "properties": { "proposal_id": { "maximum": 18446744073709551615, diff --git a/doc/schemas/query_POST_params.json b/doc/schemas/query_POST_params.json index 5b5c7289e4e..557884d5151 100644 --- a/doc/schemas/query_POST_params.json +++ b/doc/schemas/query_POST_params.json @@ -1,5 +1,4 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", "properties": { "bytecode": { "items": { diff --git a/doc/schemas/query_POST_result.json b/doc/schemas/query_POST_result.json deleted file mode 100644 index 432247374b9..00000000000 --- a/doc/schemas/query_POST_result.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "query/result" -} \ No newline at end of file diff --git a/doc/schemas/quote_GET_result.json b/doc/schemas/quote_GET_result.json index bf2633753d2..c967cc601d2 100644 --- a/doc/schemas/quote_GET_result.json +++ b/doc/schemas/quote_GET_result.json @@ -1,5 +1,4 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", "properties": { "quotes": { "items": { diff --git a/doc/schemas/quotes_GET_result.json b/doc/schemas/quotes_GET_result.json index 3840a45ba6f..800db184707 100644 --- a/doc/schemas/quotes_GET_result.json +++ b/doc/schemas/quotes_GET_result.json @@ -1,5 +1,4 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", "properties": { "quotes": { "items": { diff --git a/doc/schemas/read_POST_params.json b/doc/schemas/read_POST_params.json index cccc0bc13fb..6986faefe21 100644 --- a/doc/schemas/read_POST_params.json +++ b/doc/schemas/read_POST_params.json @@ -1,5 +1,4 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", "properties": { "key": {}, "table": { diff --git a/doc/schemas/read_POST_result.json b/doc/schemas/read_POST_result.json deleted file mode 100644 index a1e4d6a40e7..00000000000 --- a/doc/schemas/read_POST_result.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "read/result" -} \ No newline at end of file diff --git a/doc/schemas/receipt/verify_POST_params.json b/doc/schemas/receipt/verify_POST_params.json index b08f577ae7f..accc8b7c820 100644 --- a/doc/schemas/receipt/verify_POST_params.json +++ b/doc/schemas/receipt/verify_POST_params.json @@ -1,5 +1,4 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", "properties": { "receipt": { "items": { diff --git a/doc/schemas/receipt/verify_POST_result.json b/doc/schemas/receipt/verify_POST_result.json index 5825bd1cf1f..920d7652c68 100644 --- a/doc/schemas/receipt/verify_POST_result.json +++ b/doc/schemas/receipt/verify_POST_result.json @@ -1,5 +1,4 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", "properties": { "valid": { "type": "boolean" diff --git a/doc/schemas/receipt_GET_params.json b/doc/schemas/receipt_GET_params.json index ed67b676d79..cab4d390bd4 100644 --- a/doc/schemas/receipt_GET_params.json +++ b/doc/schemas/receipt_GET_params.json @@ -1,5 +1,4 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", "properties": { "commit": { "maximum": 9223372036854775807, diff --git a/doc/schemas/receipt_GET_result.json b/doc/schemas/receipt_GET_result.json index 71ec3500340..38eefacaf36 100644 --- a/doc/schemas/receipt_GET_result.json +++ b/doc/schemas/receipt_GET_result.json @@ -1,5 +1,4 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", "properties": { "receipt": { "items": { diff --git a/doc/schemas/recovery_share/submit_POST_params.json b/doc/schemas/recovery_share/submit_POST_params.json index 49c61c6fc79..d0627d76fe2 100644 --- a/doc/schemas/recovery_share/submit_POST_params.json +++ b/doc/schemas/recovery_share/submit_POST_params.json @@ -1,5 +1,4 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", "title": "recovery_share/submit/params", "type": "string" } \ No newline at end of file diff --git a/doc/schemas/recovery_share/submit_POST_result.json b/doc/schemas/recovery_share/submit_POST_result.json index 19668eb7502..8b951e5d9c8 100644 --- a/doc/schemas/recovery_share/submit_POST_result.json +++ b/doc/schemas/recovery_share/submit_POST_result.json @@ -1,5 +1,4 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", "title": "recovery_share/submit/result", "type": "string" } \ No newline at end of file diff --git a/doc/schemas/recovery_share_GET_result.json b/doc/schemas/recovery_share_GET_result.json index 4b59a933ecf..82e894be508 100644 --- a/doc/schemas/recovery_share_GET_result.json +++ b/doc/schemas/recovery_share_GET_result.json @@ -1,5 +1,4 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", "properties": { "encrypted_recovery_share": { "type": "string" diff --git a/doc/schemas/state_GET_result.json b/doc/schemas/state_GET_result.json index 9ddcdfa8baf..42031ec0696 100644 --- a/doc/schemas/state_GET_result.json +++ b/doc/schemas/state_GET_result.json @@ -1,5 +1,4 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", "properties": { "id": { "maximum": 18446744073709551615, diff --git a/doc/schemas/tx_GET_params.json b/doc/schemas/tx_GET_params.json index 96801e6ecfa..be21129895d 100644 --- a/doc/schemas/tx_GET_params.json +++ b/doc/schemas/tx_GET_params.json @@ -1,5 +1,4 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", "properties": { "seqno": { "maximum": 9223372036854775807, diff --git a/doc/schemas/tx_GET_result.json b/doc/schemas/tx_GET_result.json index 2b5a3450a87..068146dc225 100644 --- a/doc/schemas/tx_GET_result.json +++ b/doc/schemas/tx_GET_result.json @@ -1,5 +1,4 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", "properties": { "status": { "enum": [ diff --git a/doc/schemas/user_id_GET_params.json b/doc/schemas/user_id_GET_params.json index 5275cf6085b..d4babd1140b 100644 --- a/doc/schemas/user_id_GET_params.json +++ b/doc/schemas/user_id_GET_params.json @@ -1,5 +1,4 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", "properties": { "cert": { "items": { diff --git a/doc/schemas/user_id_GET_result.json b/doc/schemas/user_id_GET_result.json index 769db669917..62469fc2e84 100644 --- a/doc/schemas/user_id_GET_result.json +++ b/doc/schemas/user_id_GET_result.json @@ -1,5 +1,4 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", "properties": { "caller_id": { "maximum": 18446744073709551615, diff --git a/src/apps/js_generic/js_generic.cpp b/src/apps/js_generic/js_generic.cpp index abec4d833f9..9dc8e17494b 100644 --- a/src/apps/js_generic/js_generic.cpp +++ b/src/apps/js_generic/js_generic.cpp @@ -655,26 +655,62 @@ namespace ccfapp set_default(default_handler); } + static std::pair split_script_key( + const std::string& key) + { + size_t s = key.find(' '); + if (s != std::string::npos) + { + return std::make_pair( + http::http_method_from_str(key.substr(0, s).c_str()), + key.substr(s + 1, key.size() - (s + 1))); + } + else + { + return std::make_pair(HTTP_POST, key); + } + } + // Since we do our own dispatch within the default handler, report the // supported methods here - void list_methods(kv::Tx& tx, ListMethods::Out& out) override + void build_api(nlohmann::json& document, kv::Tx& tx) override { - UserEndpointRegistry::list_methods(tx, out); + UserEndpointRegistry::build_api(document, tx); auto scripts = tx.get_view(this->network.app_scripts); - scripts->foreach([&out](const auto& key, const auto&) { - size_t s = key.find(' '); - if (s != std::string::npos) - { - out.endpoints.push_back( - {key.substr(0, s), key.substr(s + 1, key.size() - (s + 1))}); - } - else + scripts->foreach([&document](const auto& key, const auto&) { + const auto [verb, method] = split_script_key(key); + + ds::openapi::path_operation(ds::openapi::path(document, method), verb); + return true; + }); + } + + nlohmann::json get_endpoint_schema( + kv::Tx& tx, const GetSchema::In& in) override + { + auto j = UserEndpointRegistry::get_endpoint_schema(tx, in); + + auto scripts = tx.get_view(this->network.app_scripts); + scripts->foreach([&j, &in](const auto& key, const auto&) { + const auto [verb, method] = split_script_key(key); + + if (in.method == method) { - out.endpoints.push_back({"POST", key}); + std::string verb_name = http_method_str(verb); + nonstd::to_lower(verb_name); + // We have no schema for JS endpoints, but populate the object if we + // know about them + GetSchema::Out out; + out.params_schema.schema = nullptr; + out.result_schema.schema = nullptr; + j[verb_name] = out; } + return true; }); + + return j; } }; diff --git a/src/apps/logging/logging.cpp b/src/apps/logging/logging.cpp index f617091ac91..b7b1beab2fb 100644 --- a/src/apps/logging/logging.cpp +++ b/src/apps/logging/logging.cpp @@ -69,6 +69,12 @@ namespace loggingapp get_public_params_schema(nlohmann::json::parse(j_get_public_in)), get_public_result_schema(nlohmann::json::parse(j_get_public_out)) { + openapi_info.title = "CCF Sample Logging App"; + openapi_info.description = + "This CCF sample app implements a simple logging application, securely " + "recording messages at client-specified IDs. It demonstrates most of " + "the features available to CCF apps."; + // SNIPPET_START: record auto record = [this](kv::Tx& tx, nlohmann::json&& params) { // SNIPPET_START: macro_validation_record diff --git a/src/apps/logging/logging_schema.h b/src/apps/logging/logging_schema.h index 1996e946197..1b7478ed156 100644 --- a/src/apps/logging/logging_schema.h +++ b/src/apps/logging/logging_schema.h @@ -52,7 +52,6 @@ namespace loggingapp // Manual schemas, verified then parsed in handler static const std::string j_record_public_in = R"!!!( { - "$schema": "http://json-schema.org/draft-07/schema#", "properties": { "id": { "type": "number" @@ -72,15 +71,13 @@ namespace loggingapp static const std::string j_record_public_out = R"!!!( { - "$schema": "http://json-schema.org/draft-07/schema#", "title": "log/public/result", - "type": "bool" + "type": "boolean" } )!!!"; static const std::string j_get_public_in = R"!!!( { - "$schema": "http://json-schema.org/draft-07/schema#", "properties": { "id": { "type": "number" @@ -96,7 +93,6 @@ namespace loggingapp static const std::string j_get_public_out = R"!!!( { - "$schema": "http://json-schema.org/draft-07/schema#", "properties": { "msg": { "type": "string" diff --git a/src/apps/lua_generic/lua_generic.cpp b/src/apps/lua_generic/lua_generic.cpp index e3f618e2e37..d24c28c8eec 100644 --- a/src/apps/lua_generic/lua_generic.cpp +++ b/src/apps/lua_generic/lua_generic.cpp @@ -185,17 +185,62 @@ namespace ccfapp set_default(json_adapter(default_handler)); } + static std::pair split_script_key( + const std::string& key) + { + size_t s = key.find(' '); + if (s != std::string::npos) + { + return std::make_pair( + http::http_method_from_str(key.substr(0, s).c_str()), + key.substr(s + 1, key.size() - (s + 1))); + } + else + { + return std::make_pair(HTTP_POST, key); + } + } + // Since we do our own dispatch within the default handler, report the // supported methods here - void list_methods(kv::Tx& tx, ListMethods::Out& out) override + void build_api(nlohmann::json& document, kv::Tx& tx) override + { + UserEndpointRegistry::build_api(document, tx); + + auto scripts = tx.get_view(this->network.app_scripts); + scripts->foreach([&document](const auto& key, const auto&) { + const auto [verb, method] = split_script_key(key); + + ds::openapi::path_operation(ds::openapi::path(document, method), verb); + return true; + }); + } + + nlohmann::json get_endpoint_schema( + kv::Tx& tx, const GetSchema::In& in) override { - UserEndpointRegistry::list_methods(tx, out); + auto j = UserEndpointRegistry::get_endpoint_schema(tx, in); auto scripts = tx.get_view(this->network.app_scripts); - scripts->foreach([&out](const auto& key, const auto&) { - out.endpoints.push_back({"POST", key}); + scripts->foreach([&j, &in](const auto& key, const auto&) { + const auto [verb, method] = split_script_key(key); + + if (in.method == method) + { + std::string verb_name = http_method_str(verb); + nonstd::to_lower(verb_name); + // We have no schema for JS endpoints, but populate the object if we + // know about them + GetSchema::Out out; + out.params_schema.schema = nullptr; + out.result_schema.schema = nullptr; + j[verb_name] = out; + } + return true; }); + + return j; } }; diff --git a/src/ds/json.h b/src/ds/json.h index f132c586f10..03d43cbefbe 100644 --- a/src/ds/json.h +++ b/src/ds/json.h @@ -373,6 +373,35 @@ namespace std #define FILL_SCHEMA_OPTIONAL_FOR_JSON_FINAL(TYPE, FIELD) \ FILL_SCHEMA_OPTIONAL_WITH_RENAMES_FOR_JSON_FINAL(TYPE, FIELD, FIELD) +#define ADD_SCHEMA_COMPONENTS_REQUIRED_WITH_RENAMES_FOR_JSON_NEXT( \ + TYPE, C_FIELD, JSON_FIELD) \ + j["properties"][#JSON_FIELD] = \ + doc.template add_schema_component(); \ + j["required"].push_back(#JSON_FIELD); +#define ADD_SCHEMA_COMPONENTS_REQUIRED_WITH_RENAMES_FOR_JSON_FINAL( \ + TYPE, C_FIELD, JSON_FIELD) \ + ADD_SCHEMA_COMPONENTS_REQUIRED_WITH_RENAMES_FOR_JSON_NEXT( \ + TYPE, C_FIELD, JSON_FIELD) + +#define ADD_SCHEMA_COMPONENTS_REQUIRED_FOR_JSON_NEXT(TYPE, FIELD) \ + ADD_SCHEMA_COMPONENTS_REQUIRED_WITH_RENAMES_FOR_JSON_NEXT(TYPE, FIELD, FIELD) +#define ADD_SCHEMA_COMPONENTS_REQUIRED_FOR_JSON_FINAL(TYPE, FIELD) \ + ADD_SCHEMA_COMPONENTS_REQUIRED_WITH_RENAMES_FOR_JSON_FINAL(TYPE, FIELD, FIELD) + +#define ADD_SCHEMA_COMPONENTS_OPTIONAL_WITH_RENAMES_FOR_JSON_NEXT( \ + TYPE, C_FIELD, JSON_FIELD) \ + j["properties"][#JSON_FIELD] = \ + doc.template add_schema_component(); +#define ADD_SCHEMA_COMPONENTS_OPTIONAL_WITH_RENAMES_FOR_JSON_FINAL( \ + TYPE, C_FIELD, JSON_FIELD) \ + ADD_SCHEMA_COMPONENTS_OPTIONAL_WITH_RENAMES_FOR_JSON_NEXT( \ + TYPE, C_FIELD, JSON_FIELD) + +#define ADD_SCHEMA_COMPONENTS_OPTIONAL_FOR_JSON_NEXT(TYPE, FIELD) \ + ADD_SCHEMA_COMPONENTS_OPTIONAL_WITH_RENAMES_FOR_JSON_NEXT(TYPE, FIELD, FIELD) +#define ADD_SCHEMA_COMPONENTS_OPTIONAL_FOR_JSON_FINAL(TYPE, FIELD) \ + ADD_SCHEMA_COMPONENTS_OPTIONAL_WITH_RENAMES_FOR_JSON_FINAL(TYPE, FIELD, FIELD) + #define JSON_FIELD_FOR_JSON_NEXT(TYPE, FIELD) \ JsonField{#FIELD}, #define JSON_FIELD_FOR_JSON_FINAL(TYPE, FIELD) \ @@ -381,16 +410,24 @@ namespace std # FIELD \ } -/** Defines from_json, to_json, and fill_json_schema functions for struct/class - * types, converting member fields to JSON elements. Missing elements will cause - * errors to be raised. This assumes that from_json, to_json, and - * fill_json_schema are implemented for each member field type, either manually - * or through these macros. +/** Defines from_json, to_json, fill_json_schema, and schema_name functions for + * struct/class types, converting member fields to JSON elements. Missing + * elements will cause errors to be raised. This assumes that from_json, + * to_json, and fill_json_schema are implemented for each member field type, + * either manually or through these macros. + * // clang-format off * ie, the following must compile, for each foo in T: * T t; nlohmann::json j, schema; * j["foo"] = t.foo; * t.foo = j["foo"].get(); * fill_json_schema(schema, t); + * std::string s = schema_name(t.foo); + * // clang-format on + * + * Optional fields will be inserted into the JSON object iff their value differs + * from the value in a default-constructed instance of T. So if optional fields + * are present, then T must be default-constructible and the optional fields + * must be distinguishable (have operator!= defined) * * To use: * - Declare struct as normal @@ -479,13 +516,21 @@ namespace std PRE_FROM_JSON, \ POST_FROM_JSON, \ PRE_FILL_SCHEMA, \ - POST_FILL_SCHEMA) \ + POST_FILL_SCHEMA, \ + PRE_ADD_SCHEMA, \ + POST_ADD_SCHEMA) \ void to_json_required_fields(nlohmann::json& j, const TYPE& t); \ void to_json_optional_fields(nlohmann::json& j, const TYPE& t); \ void from_json_required_fields(const nlohmann::json& j, TYPE& t); \ void from_json_optional_fields(const nlohmann::json& j, TYPE& t); \ void fill_json_schema_required_fields(nlohmann::json& j, const TYPE& t); \ void fill_json_schema_optional_fields(nlohmann::json& j, const TYPE& t); \ + template \ + void add_schema_components_required_fields( \ + T& doc, nlohmann::json& j, const TYPE& t); \ + template \ + void add_schema_components_optional_fields( \ + T& doc, nlohmann::json& j, const TYPE& t); \ inline void to_json(nlohmann::json& j, const TYPE& t) \ { \ PRE_TO_JSON; \ @@ -503,9 +548,20 @@ namespace std PRE_FILL_SCHEMA; \ fill_json_schema_required_fields(j, t); \ POST_FILL_SCHEMA; \ + } \ + inline std::string schema_name(const TYPE&) \ + { \ + return #TYPE; \ + } \ + template \ + void add_schema_components(T& doc, nlohmann::json& j, const TYPE& t) \ + { \ + PRE_ADD_SCHEMA; \ + add_schema_components_required_fields(doc, j, t); \ + POST_ADD_SCHEMA; \ } -#define DECLARE_JSON_TYPE(TYPE) DECLARE_JSON_TYPE_IMPL(TYPE, , , , , , ) +#define DECLARE_JSON_TYPE(TYPE) DECLARE_JSON_TYPE_IMPL(TYPE, , , , , , , , ) #define DECLARE_JSON_TYPE_WITH_BASE(TYPE, BASE) \ DECLARE_JSON_TYPE_IMPL( \ @@ -514,7 +570,9 @@ namespace std , \ from_json(j, static_cast(t)), \ , \ - fill_json_schema(j, static_cast(t)), ) + fill_json_schema(j, static_cast(t)), \ + , \ + add_schema_components(doc, j, static_cast(t)), ) #define DECLARE_JSON_TYPE_WITH_OPTIONAL_FIELDS(TYPE) \ DECLARE_JSON_TYPE_IMPL( \ @@ -524,7 +582,9 @@ namespace std , \ from_json_optional_fields(j, t), \ , \ - fill_json_schema_optional_fields(j, t)) + fill_json_schema_optional_fields(j, t), \ + , \ + add_schema_components_optional_fields(doc, j, t)) #define DECLARE_JSON_TYPE_WITH_BASE_AND_OPTIONAL_FIELDS(TYPE, BASE) \ DECLARE_JSON_TYPE_IMPL( \ @@ -534,10 +594,13 @@ namespace std from_json(j, static_cast(t)), \ from_json_optional_fields(j, t), \ fill_json_schema(j, static_cast(t)), \ - fill_json_schema_optional_fields(j, t)) + fill_json_schema_optional_fields(j, t), \ + add_schema_components(doc, j, static_cast(t)), \ + add_schema_components_optional_fields(doc, j, t)) #define DECLARE_JSON_REQUIRED_FIELDS(TYPE, ...) \ - inline void to_json_required_fields(nlohmann::json& j, const TYPE& t) \ + inline void to_json_required_fields( \ + nlohmann::json& j, [[maybe_unused]] const TYPE& t) \ { \ if (!j.is_object()) \ { \ @@ -545,7 +608,8 @@ namespace std } \ _FOR_JSON_COUNT_NN(__VA_ARGS__)(POP1)(WRITE_REQUIRED, TYPE, ##__VA_ARGS__) \ } \ - inline void from_json_required_fields(const nlohmann::json& j, TYPE& t) \ + inline void from_json_required_fields( \ + const nlohmann::json& j, [[maybe_unused]] TYPE& t) \ { \ if (!j.is_object()) \ { \ @@ -553,11 +617,22 @@ namespace std } \ _FOR_JSON_COUNT_NN(__VA_ARGS__)(POP1)(READ_REQUIRED, TYPE, ##__VA_ARGS__) \ } \ - inline void fill_json_schema_required_fields(nlohmann::json& j, const TYPE&) \ + inline void fill_json_schema_required_fields( \ + nlohmann::json& j, [[maybe_unused]] const TYPE& t) \ { \ j["type"] = "object"; \ _FOR_JSON_COUNT_NN(__VA_ARGS__) \ (POP1)(FILL_SCHEMA_REQUIRED, TYPE, ##__VA_ARGS__) \ + } \ + template \ + void add_schema_components_required_fields( \ + [[maybe_unused]] T& doc, \ + nlohmann::json& j, \ + [[maybe_unused]] const TYPE& t) \ + { \ + j["type"] = "object"; \ + _FOR_JSON_COUNT_NN(__VA_ARGS__) \ + (POP1)(ADD_SCHEMA_COMPONENTS_REQUIRED, TYPE, ##__VA_ARGS__); \ } #define DECLARE_JSON_REQUIRED_FIELDS_WITH_RENAMES(TYPE, ...) \ @@ -584,6 +659,14 @@ namespace std j["type"] = "object"; \ _FOR_JSON_COUNT_NN(__VA_ARGS__) \ (POP2)(FILL_SCHEMA_REQUIRED_WITH_RENAMES, TYPE, ##__VA_ARGS__) \ + } \ + template \ + void add_schema_components_required_fields( \ + T& doc, nlohmann::json& j, const TYPE& t) \ + { \ + j["type"] = "object"; \ + _FOR_JSON_COUNT_NN(__VA_ARGS__) \ + (POP2)(ADD_SCHEMA_COMPONENTS_REQUIRED_WITH_RENAMES, TYPE, ##__VA_ARGS__); \ } #define DECLARE_JSON_OPTIONAL_FIELDS(TYPE, ...) \ @@ -600,6 +683,13 @@ namespace std { \ _FOR_JSON_COUNT_NN(__VA_ARGS__) \ (POP1)(FILL_SCHEMA_OPTIONAL, TYPE, ##__VA_ARGS__) \ + } \ + template \ + void add_schema_components_optional_fields( \ + T& doc, nlohmann::json& j, const TYPE&) \ + { \ + _FOR_JSON_COUNT_NN(__VA_ARGS__) \ + (POP1)(ADD_SCHEMA_COMPONENTS_OPTIONAL, TYPE, ##__VA_ARGS__); \ } #define DECLARE_JSON_OPTIONAL_FIELDS_WITH_RENAMES(TYPE, ...) \ @@ -619,10 +709,21 @@ namespace std { \ _FOR_JSON_COUNT_NN(__VA_ARGS__) \ (POP2)(FILL_SCHEMA_OPTIONAL_WITH_RENAMES, TYPE, ##__VA_ARGS__) \ + } \ + template \ + void add_schema_components_optional_fields( \ + T& doc, nlohmann::json& j, const TYPE& t) \ + { \ + _FOR_JSON_COUNT_NN(__VA_ARGS__) \ + (POP2)(ADD_SCHEMA_COMPONENTS_OPTIONAL_WITH_RENAMES, TYPE, ##__VA_ARGS__); \ } #define DECLARE_JSON_ENUM(TYPE, ...) \ NLOHMANN_JSON_SERIALIZE_ENUM(TYPE, __VA_ARGS__) \ + inline std::string schema_name(const TYPE&) \ + { \ + return #TYPE; \ + } \ inline void fill_enum_schema(nlohmann::json& j, const TYPE&) \ { \ static const std::pair m[] = __VA_ARGS__; \ diff --git a/src/ds/json_schema.h b/src/ds/json_schema.h index ccefe2a14b1..7a3045dd2be 100644 --- a/src/ds/json_schema.h +++ b/src/ds/json_schema.h @@ -4,6 +4,8 @@ #include "ds/nonstd.h" +#define FMT_HEADER_ONLY +#include #include namespace ds @@ -36,6 +38,9 @@ namespace ds schema["maximum"] = std::numeric_limits::max(); } + template + std::string schema_name(); + template void fill_schema(nlohmann::json& schema); @@ -50,8 +55,23 @@ namespace ds return element; } + template + nlohmann::json schema_element() + { + auto element = nlohmann::json::object(); + fill_schema(element); + return element; + } + namespace adl { + template + std::string schema_name() + { + T t; + return schema_name(t); + } + template void fill_schema(nlohmann::json& schema) { @@ -67,6 +87,103 @@ namespace ds } } + template + inline std::string schema_name() + { + if constexpr (nonstd::is_specialization::value) + { + return schema_name(); + } + else if constexpr (nonstd::is_specialization::value) + { + return fmt::format("{}_array", schema_name()); + } + else if constexpr ( + nonstd::is_specialization::value || + nonstd::is_specialization::value) + { + if (std::is_same::value) + { + return fmt::format( + "named_{}", schema_name()); + } + else + { + return fmt::format( + "{}_to_{}", + schema_name(), + schema_name()); + } + } + else if constexpr (nonstd::is_specialization::value) + { + return fmt::format( + "{}_and_{}", + schema_name(), + schema_name()); + } + else if constexpr (std::is_same::value) + { + return "string"; + } + else if constexpr (std::is_same::value) + { + return "boolean"; + } + else if constexpr (std::is_same::value) + { + return "uint8"; + } + else if constexpr (std::is_same::value) + { + return "uint16"; + } + else if constexpr (std::is_same::value) + { + return "uint32"; + } + else if constexpr (std::is_same::value) + { + return "uint64"; + } + else if constexpr (std::is_same::value) + { + return "int8"; + } + else if constexpr (std::is_same::value) + { + return "int16"; + } + else if constexpr (std::is_same::value) + { + return "int32"; + } + else if constexpr (std::is_same::value) + { + return "int64"; + } + else if constexpr (std::is_same::value) + { + return "float"; + } + else if constexpr (std::is_same::value) + { + return "double"; + } + else if constexpr (std::is_same::value) + { + return "json"; + } + else if constexpr (std::is_same::value) + { + return "json_schema"; + } + else + { + return adl::schema_name(); + } + } + template inline void fill_schema(nlohmann::json& schema) { @@ -83,18 +200,28 @@ namespace ds nonstd::is_specialization::value || nonstd::is_specialization::value) { - // Nlohmann serialises maps to an array of (K, V) pairs - schema["type"] = "array"; - auto items = nlohmann::json::object(); + // Nlohmann serialises maps to an array of (K, V) pairs... + if (std::is_same::value) { - items["type"] = "array"; + // ...unless the keys are strings! + schema["type"] = "object"; + schema["additionalProperties"] = + schema_element(); + } + else + { + schema["type"] = "array"; + auto items = nlohmann::json::object(); + { + items["type"] = "array"; - auto sub_items = nlohmann::json::array(); - sub_items.push_back(schema_element()); - sub_items.push_back(schema_element()); - items["items"] = sub_items; + auto sub_items = nlohmann::json::array(); + sub_items.push_back(schema_element()); + sub_items.push_back(schema_element()); + items["items"] = sub_items; + } + schema["items"] = items; } - schema["items"] = items; } else if constexpr (nonstd::is_specialization::value) { @@ -116,6 +243,7 @@ namespace ds { // Any field that contains more json is completely unconstrained, so we // do not add a type or any other fields + schema = nlohmann::json::object(); } else if constexpr (std::is_integral::value) { @@ -127,7 +255,7 @@ namespace ds } else if constexpr (std::is_same::value) { - schema["$ref"] = JsonSchema::hyperschema; + schema["type"] = "object"; } else { @@ -139,7 +267,6 @@ namespace ds inline nlohmann::json build_schema(const std::string& title) { nlohmann::json schema; - schema["$schema"] = JsonSchema::hyperschema; schema["title"] = title; fill_schema(schema); diff --git a/src/ds/nonstd.h b/src/ds/nonstd.h index d16a52f2c40..ae8d1c97bda 100644 --- a/src/ds/nonstd.h +++ b/src/ds/nonstd.h @@ -2,7 +2,9 @@ // Licensed under the Apache 2.0 License. #pragma once +#include #include +#include #include #include #include @@ -59,4 +61,31 @@ namespace nonstd template using remove_cvref_t = typename remove_cvref::type; + + /** converts strings to upper or lower case, in-place + */ + static inline void to_upper(std::string& s) + { + std::transform(s.begin(), s.end(), s.begin(), [](unsigned char c) { + return std::toupper(c); + }); + } + + static inline void to_lower(std::string& s) + { + std::transform(s.begin(), s.end(), s.begin(), [](unsigned char c) { + return std::tolower(c); + }); + } + + static inline std::string remove_prefix( + const std::string& s, const std::string& prefix) + { + if (s.find(prefix) == 0) + { + return s.substr(prefix.size()); + } + + return s; + } } \ No newline at end of file diff --git a/src/ds/openapi.h b/src/ds/openapi.h new file mode 100644 index 00000000000..323ea4b20ea --- /dev/null +++ b/src/ds/openapi.h @@ -0,0 +1,387 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache 2.0 License. +#pragma once + +#include "ds/json.h" +#include "ds/nonstd.h" + +#include +#include +#include + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wgnu-zero-variadic-macro-arguments" + +namespace ds +{ + /** + * This namespace contains helper functions, structs, and templates for + * modifying an OpenAPI JSON document. They do not set every field, but should + * fill every _required_ field, and the resulting object can be further + * modified by hand as required. + */ + namespace openapi + { + namespace access + { + static inline nlohmann::json& get_object( + nlohmann::json& j, const std::string& k) + { + const auto ib = j.emplace(k, nlohmann::json::object()); + return ib.first.value(); + } + + static inline nlohmann::json& get_array( + nlohmann::json& j, const std::string& k) + { + const auto ib = j.emplace(k, nlohmann::json::array()); + return ib.first.value(); + } + } + + static inline void check_path_valid(const std::string& s) + { + if (s.rfind("/", 0) != 0) + { + throw std::logic_error( + fmt::format("'{}' is not a valid path - must begin with '/'", s)); + } + } + + static inline std::string remove_invalid_chars(const std::string_view& s_) + { + std::string s(s_); + + for (auto& c : s) + { + if (c == ':') + { + c = '_'; + } + } + + return s; + } + + static inline nlohmann::json create_document( + const std::string& title, + const std::string& description, + const std::string& document_version) + { + return nlohmann::json{{"openapi", "3.0.0"}, + {"info", + {{"title", title}, + {"description", description}, + {"version", document_version}}}, + {"servers", nlohmann::json::array()}, + {"paths", nlohmann::json::object()}}; + } + + static inline nlohmann::json& server( + nlohmann::json& document, const std::string& url) + { + auto& servers = access::get_object(document, "servers"); + servers.push_back({{"url", url}}); + return servers.back(); + } + + static inline nlohmann::json& path( + nlohmann::json& document, const std::string& path) + { + auto p = remove_invalid_chars(path); + if (p.find("/") != 0) + { + p = fmt::format("/{}", p); + } + + auto& paths = access::get_object(document, "paths"); + return access::get_object(paths, p); + } + + static inline nlohmann::json& path_operation( + nlohmann::json& path, http_method verb) + { + // HTTP_GET becomes the string "get" + std::string s = http_method_str(verb); + nonstd::to_lower(s); + auto& po = access::get_object(path, s); + // responses is required field in a path_operation + access::get_object(po, "responses"); + return po; + } + + static inline nlohmann::json& parameters(nlohmann::json& path_operation) + { + return access::get_array(path_operation, "parameters"); + } + + static inline nlohmann::json& response( + nlohmann::json& path_operation, + http_status status, + const std::string& description = "Default response description") + { + auto& responses = access::get_object(path_operation, "responses"); + // HTTP_STATUS_OK (aka an int-enum with value 200) becomes the string + // "200" + const auto s = std::to_string(status); + auto& response = access::get_object(responses, s); + response["description"] = description; + return response; + } + + static inline nlohmann::json& request_body(nlohmann::json& path_operation) + { + auto& request_body = access::get_object(path_operation, "requestBody"); + access::get_object(request_body, "content"); + return request_body; + } + + static inline nlohmann::json& media_type( + nlohmann::json& j, const std::string& mt) + { + auto& content = access::get_object(j, "content"); + return access::get_object(content, mt); + } + + static inline nlohmann::json& schema(nlohmann::json& media_type_object) + { + return access::get_object(media_type_object, "schema"); + } + + // + // Helper functions for auto-inserting schema into components + // + + static inline nlohmann::json components_ref_object( + const std::string& element_name) + { + auto schema_ref_object = nlohmann::json::object(); + schema_ref_object["$ref"] = + fmt::format("#/components/schemas/{}", element_name); + return schema_ref_object; + } + + // Returns a ref object pointing to the item inserted into the components + static inline nlohmann::json add_schema_to_components( + nlohmann::json& document, + const std::string& element_name, + const nlohmann::json& schema_) + { + const auto name = remove_invalid_chars(element_name); + + auto& components = access::get_object(document, "components"); + auto& schemas = access::get_object(components, "schemas"); + + const auto schema_it = schemas.find(name); + if (schema_it != schemas.end()) + { + // Check that the existing schema matches the new one being added with + // the same name + const auto& existing_schema = schema_it.value(); + if (schema_ != existing_schema) + { + throw std::logic_error(fmt::format( + "Adding schema with name '{}'. Does not match previous schema " + "registered with this name: {} vs {}", + name, + schema_.dump(), + existing_schema.dump())); + } + } + else + { + schemas.emplace(name, schema_); + } + + return components_ref_object(name); + } + + struct SchemaHelper + { + nlohmann::json& document; + + template + nlohmann::json add_schema_component() + { + nlohmann::json schema; + if constexpr (nonstd::is_specialization::value) + { + return add_schema_component(); + } + else if constexpr (nonstd::is_specialization::value) + { + schema["type"] = "array"; + schema["items"] = add_schema_component(); + + return add_schema_to_components( + document, ds::json::schema_name(), schema); + } + else if constexpr ( + nonstd::is_specialization::value || + nonstd::is_specialization::value) + { + // Nlohmann serialises maps to an array of (K, V) pairs + if (std::is_same::value) + { + // ...unless the keys are strings! + schema["type"] = "object"; + schema["additionalProperties"] = + add_schema_component(); + } + else + { + schema["type"] = "array"; + auto items = nlohmann::json::object(); + { + items["type"] = "array"; + + auto sub_items = nlohmann::json::array(); + // NB: OpenAPI doesn't like this tuple for "items", even though + // its valid JSON schema. May need to switch this to oneOf to + // satisfy some validators + sub_items.push_back(add_schema_component()); + sub_items.push_back( + add_schema_component()); + items["items"] = sub_items; + } + schema["items"] = items; + } + return add_schema_to_components( + document, ds::json::schema_name(), schema); + } + else if constexpr (nonstd::is_specialization::value) + { + schema["type"] = "array"; + auto items = nlohmann::json::array(); + items.push_back(add_schema_component()); + items.push_back(add_schema_component()); + schema["items"] = items; + return add_schema_to_components( + document, ds::json::schema_name(), schema); + } + else if constexpr ( + std::is_same::value || std::is_arithmetic_v || + std::is_same::value || + std::is_same::value) + { + ds::json::fill_schema(schema); + return add_schema_to_components( + document, ds::json::schema_name(), schema); + } + else + { + const auto name = remove_invalid_chars(ds::json::schema_name()); + + auto& components = access::get_object(document, "components"); + auto& schemas = access::get_object(components, "schemas"); + + const auto ib = schemas.emplace(name, nlohmann::json::object()); + if (ib.second) + { + auto& j = ib.first.value(); + + // Use argument-dependent-lookup to call correct functions + T t; + if constexpr (std::is_enum::value) + { + fill_enum_schema(j, t); + } + else + { + add_schema_components(*this, j, t); + } + } + + return components_ref_object(name); + } + } + }; + + static inline void add_request_body_schema( + nlohmann::json& document, + const std::string& uri, + http_method verb, + const std::string& content_type, + const std::string& schema_name, + const nlohmann::json& schema_) + { + auto& rb = request_body(path_operation(path(document, uri), verb)); + rb["description"] = "Auto-generated request body schema"; + + schema(media_type(rb, content_type)) = + add_schema_to_components(document, schema_name, schema_); + } + + template + static inline void add_request_body_schema( + nlohmann::json& document, + const std::string& uri, + http_method verb, + const std::string& content_type) + { + auto& rb = request_body(path_operation(path(document, uri), verb)); + rb["description"] = "Auto-generated request body schema"; + + SchemaHelper sh{document}; + const auto schema_comp = sh.add_schema_component(); + if (schema_comp != nullptr) + { + schema(media_type(rb, content_type)) = sh.add_schema_component(); + } + } + + static inline void add_path_parameter_schema( + nlohmann::json& document, + const std::string& uri, + const nlohmann::json& param) + { + auto& params = parameters(path(document, uri)); + params.push_back(param); + } + + static inline void add_request_parameter_schema( + nlohmann::json& document, + const std::string& uri, + http_method verb, + const nlohmann::json& param) + { + auto& params = parameters(path_operation(path(document, uri), verb)); + params.push_back(param); + } + + static inline void add_response_schema( + nlohmann::json& document, + const std::string& uri, + http_method verb, + http_status status, + const std::string& content_type, + const std::string& schema_name, + const nlohmann::json& schema_) + { + auto& r = response(path_operation(path(document, uri), verb), status); + + schema(media_type(r, content_type)) = + add_schema_to_components(document, schema_name, schema_); + } + + template + static inline void add_response_schema( + nlohmann::json& document, + const std::string& uri, + http_method verb, + http_status status, + const std::string& content_type) + { + auto& r = response(path_operation(path(document, uri), verb), status); + + SchemaHelper sh{document}; + const auto schema_comp = sh.add_schema_component(); + if (schema_comp != nullptr) + { + schema(media_type(r, content_type)) = sh.add_schema_component(); + } + } + } +} + +#pragma clang diagnostic pop diff --git a/src/ds/test/openapi.cpp b/src/ds/test/openapi.cpp new file mode 100644 index 00000000000..00f1a7186c3 --- /dev/null +++ b/src/ds/test/openapi.cpp @@ -0,0 +1,214 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache 2.0 License. +#include "ds/openapi.h" + +#include "http/http_consts.h" + +#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN +#include + +using namespace ds; + +void print_doc(const std::string& title, const nlohmann::json& doc) +{ + std::cout << title << std::endl; + std::cout << doc.dump(2) << std::endl; +} + +#define REQUIRE_ELEMENT(j, name, type_fn) \ + { \ + const auto name##_it = j.find(#name); \ + REQUIRE(name##_it != j.end()); \ + REQUIRE(name##_it->type_fn()); \ + } + +static constexpr auto server_url = "https://not.a.real.server.com/testing_only"; + +// This is only a very basic check - assume full validation is done by external +// validator +void required_doc_elements(const nlohmann::json& j) +{ + REQUIRE_ELEMENT(j, openapi, is_string); + REQUIRE_ELEMENT(j, info, is_object); + REQUIRE_ELEMENT(j, paths, is_object); +} + +// TEST_CASE("Required elements") +// { +// openapi::Document doc; + +// const nlohmann::json j = doc; +// required_doc_elements(j); +// } + +TEST_CASE("Manual construction") +{ + auto doc = openapi::create_document( + "Test generated API", + "Some longer description enhanced with **Markdown**", + "0.1.42"); + + openapi::server(doc, server_url); + + const auto string_schema = nlohmann::json{{"type", "string"}}; + + auto& foo = openapi::path(doc, "/users/foo"); + auto& foo_post = openapi::path_operation(foo, HTTP_POST); + auto& foo_post_request = openapi::request_body(foo_post); + auto& foo_post_request_json = openapi::media_type( + foo_post_request, http::headervalues::contenttype::JSON); + auto& foo_post_request_json_schema = openapi::schema(foo_post_request_json); + foo_post_request_json_schema = string_schema; + + auto& foo_post_response_ok = openapi::response( + foo_post, HTTP_STATUS_OK, "Indicates that everything went ok"); + auto& foo_post_response_ok_json = openapi::media_type( + foo_post_response_ok, http::headervalues::contenttype::JSON); + auto& foo_post_response_ok_json_schema = + openapi::schema(foo_post_response_ok_json); + foo_post_response_ok_json_schema = string_schema; + + required_doc_elements(doc); + + const auto& info_element = doc["info"]; + REQUIRE_ELEMENT(info_element, title, is_string); + REQUIRE_ELEMENT(info_element, description, is_string); + REQUIRE_ELEMENT(info_element, version, is_string); + + REQUIRE_ELEMENT(doc, servers, is_array); + const auto& servers_element = doc["servers"]; + REQUIRE(servers_element.size() == 1); + const auto& first_server = servers_element[0]; + REQUIRE_ELEMENT(first_server, url, is_string); +} + +struct Foo +{ + size_t n; + std::string s; +}; +DECLARE_JSON_TYPE(Foo); +DECLARE_JSON_REQUIRED_FIELDS(Foo, n, s); + +TEST_CASE("Simple custom types") +{ + auto doc = openapi::create_document( + "Test generated API", + "Some longer description enhanced with **Markdown**", + "0.1.42"); + + openapi::server(doc, server_url); + + openapi::add_request_body_schema( + doc, "/app/foo", HTTP_POST, http::headervalues::contenttype::JSON); + openapi::add_response_schema( + doc, + "/app/foo", + HTTP_POST, + HTTP_STATUS_OK, + http::headervalues::contenttype::JSON); + openapi::add_response_schema( + doc, + "/app/foo", + HTTP_POST, + HTTP_STATUS_OK, + http::headervalues::contenttype::JSON); + + required_doc_elements(doc); +} + +struct Bar +{ + std::string name; + double f; +}; +DECLARE_JSON_TYPE_WITH_OPTIONAL_FIELDS(Bar); +DECLARE_JSON_REQUIRED_FIELDS(Bar, name); +DECLARE_JSON_OPTIONAL_FIELDS(Bar, f); + +enum class Vehicle +{ + Car, + Pedalo, + Submarine, +}; + +DECLARE_JSON_ENUM( + Vehicle, + {{Vehicle::Car, "vroom vroom"}, + {Vehicle::Pedalo, "splash splash"}, + {Vehicle::Submarine, "glug glug"}}); + +struct Baz : public Bar +{ + uint16_t n; + double x; + double y; + Vehicle v; +}; +DECLARE_JSON_TYPE_WITH_BASE_AND_OPTIONAL_FIELDS(Baz, Bar); +DECLARE_JSON_REQUIRED_FIELDS(Baz, n, v); +DECLARE_JSON_OPTIONAL_FIELDS(Baz, x, y); + +struct Buzz : public Baz +{ + Foo required_and_only_in_c; + uint16_t optional_and_only_in_c; +}; +DECLARE_JSON_TYPE_WITH_BASE_AND_OPTIONAL_FIELDS(Buzz, Baz); +DECLARE_JSON_REQUIRED_FIELDS_WITH_RENAMES( + Buzz, required_and_only_in_c, RequiredJsonField); +DECLARE_JSON_OPTIONAL_FIELDS_WITH_RENAMES( + Buzz, optional_and_only_in_c, OptionalJsonField); + +TEST_CASE("Complex custom types") +{ + auto doc = openapi::create_document( + "Test generated API", + "Some longer description enhanced with **Markdown**", + "0.1.42"); + + openapi::server(doc, server_url); + + openapi::add_response_schema>( + doc, + "/app/foos", + HTTP_GET, + HTTP_STATUS_OK, + http::headervalues::contenttype::JSON); + openapi::add_response_schema>>( + doc, + "/app/fooss", + HTTP_GET, + HTTP_STATUS_OK, + http::headervalues::contenttype::JSON); + openapi::add_response_schema( + doc, + "/app/bar", + HTTP_GET, + HTTP_STATUS_OK, + http::headervalues::contenttype::JSON); + openapi::add_response_schema( + doc, + "/app/baz", + HTTP_GET, + HTTP_STATUS_OK, + http::headervalues::contenttype::JSON); + openapi::add_response_schema>( + doc, + "/app/buzz", + HTTP_GET, + HTTP_STATUS_OK, + http::headervalues::contenttype::JSON); + + openapi::add_request_body_schema>( + doc, "/app/complex", HTTP_POST, http::headervalues::contenttype::JSON); + openapi::add_response_schema>>( + doc, + "/app/complex", + HTTP_POST, + HTTP_STATUS_OK, + http::headervalues::contenttype::JSON); + + required_doc_elements(doc); +} \ No newline at end of file diff --git a/src/enclave/rpc_context.h b/src/enclave/rpc_context.h index 3a337a38cde..0a672ed3dd4 100644 --- a/src/enclave/rpc_context.h +++ b/src/enclave/rpc_context.h @@ -35,6 +35,16 @@ namespace ccf RESTVerb(const http_method& hm) : verb(hm) {} RESTVerb(const ws::Verb& wv) : verb(wv) {} + std::optional get_http_method() const + { + if (verb == ws::WEBSOCKET) + { + return std::nullopt; + } + + return static_cast(verb); + } + const char* c_str() const { if (verb == ws::WEBSOCKET) diff --git a/src/http/http_builder.h b/src/http/http_builder.h index 1941a9ea908..bc83970ae2d 100644 --- a/src/http/http_builder.h +++ b/src/http/http_builder.h @@ -63,9 +63,7 @@ namespace http void set_header(std::string k, const std::string& v) { // Store all headers lower-cased to simplify case-insensitive lookup - std::transform(k.begin(), k.end(), k.begin(), [](unsigned char c) { - return std::tolower(c); - }); + nonstd::to_lower(k); headers[k] = v; } diff --git a/src/http/http_parser.h b/src/http/http_parser.h index 169f024f66f..46064543b8a 100644 --- a/src/http/http_parser.h +++ b/src/http/http_parser.h @@ -279,9 +279,7 @@ namespace http // HTTP headers are stored lowercase as it is easier to verify HTTP // signatures later on auto f = std::string(at, length); - std::transform(f.begin(), f.end(), f.begin(), [](unsigned char c) { - return std::tolower(c); - }); + nonstd::to_lower(f); partial_parsed_header.first.append(f); } diff --git a/src/http/http_sig.h b/src/http/http_sig.h index bbee30efcdd..d510a4b7cee 100644 --- a/src/http/http_sig.h +++ b/src/http/http_sig.h @@ -32,10 +32,7 @@ namespace http if (f == auth::SIGN_HEADER_REQUEST_TARGET) { // Store verb as lowercase - std::transform( - verb.begin(), verb.end(), verb.begin(), [](unsigned char c) { - return std::tolower(c); - }); + nonstd::to_lower(verb); value = fmt::format("{} {}", verb, path); if (!query.empty()) { diff --git a/src/http/test/http_test.cpp b/src/http/test/http_test.cpp index 8efc68e19cc..198300437fc 100644 --- a/src/http/test/http_test.cpp +++ b/src/http/test/http_test.cpp @@ -24,9 +24,7 @@ std::vector s_to_v(char const* s) std::string to_lowercase(std::string s) { - std::transform(s.begin(), s.end(), s.begin(), [](unsigned char c) { - return std::tolower(c); - }); + nonstd::to_lower(s); return s; } diff --git a/src/node/rpc/call_types.h b/src/node/rpc/call_types.h index 72988c6eeb6..b950bed755c 100644 --- a/src/node/rpc/call_types.h +++ b/src/node/rpc/call_types.h @@ -132,18 +132,9 @@ namespace ccf using Out = CallerInfo; }; - struct ListMethods + struct GetAPI { - struct Endpoint - { - std::string verb; - std::string path; - }; - - struct Out - { - std::vector endpoints; - }; + using Out = nlohmann::json; }; struct EndpointMetrics diff --git a/src/node/rpc/common_endpoint_registry.h b/src/node/rpc/common_endpoint_registry.h index ff50691d37e..fb85fbee2a4 100644 --- a/src/node/rpc/common_endpoint_registry.h +++ b/src/node/rpc/common_endpoint_registry.h @@ -27,8 +27,10 @@ namespace ccf public: CommonEndpointRegistry( - kv::Store& store, const std::string& certs_table_name = "") : - EndpointRegistry(store, certs_table_name), + const std::string& method_prefix_, + kv::Store& store, + const std::string& certs_table_name = "") : + EndpointRegistry(method_prefix_, store, certs_table_name), nodes(store.get(Tables::NODES)), node_code_ids(store.get(Tables::NODE_CODE_IDS)), tables(&store) @@ -221,14 +223,16 @@ namespace ccf .set_auto_schema() .install(); - auto list_methods_fn = [this](kv::Tx& tx, nlohmann::json&&) { - ListMethods::Out out; - list_methods(tx, out); - - return make_success(out); + auto openapi = [this](kv::Tx& tx, nlohmann::json&&) { + auto document = ds::openapi::create_document( + openapi_info.title, + openapi_info.description, + openapi_info.document_version); + build_api(document, tx); + return make_success(document); }; - make_endpoint("api", HTTP_GET, json_adapter(list_methods_fn)) - .set_auto_schema() + make_endpoint("api", HTTP_GET, json_adapter(openapi)) + .set_auto_schema() .install(); auto endpoint_metrics_fn = [this](kv::Tx& tx, nlohmann::json&&) { @@ -242,43 +246,10 @@ namespace ccf .set_auto_schema() .install(); - auto get_schema = [this](auto&, nlohmann::json&& params) { + auto get_schema = [this](kv::Tx& tx, nlohmann::json&& params) { const auto in = params.get(); - auto j = nlohmann::json::object(); - - const auto it = fully_qualified_endpoints.find(in.method); - if (it != fully_qualified_endpoints.end()) - { - for (const auto& [verb, endpoint] : it->second) - { - std::string verb_name = verb.c_str(); - std::transform( - verb_name.begin(), - verb_name.end(), - verb_name.begin(), - [](unsigned char c) { return std::tolower(c); }); - j[verb_name] = - GetSchema::Out{endpoint.params_schema, endpoint.result_schema}; - } - } - - const auto templated_it = templated_endpoints.find(in.method); - if (templated_it != templated_endpoints.end()) - { - for (const auto& [verb, endpoint] : templated_it->second) - { - std::string verb_name = verb.c_str(); - std::transform( - verb_name.begin(), - verb_name.end(), - verb_name.begin(), - [](unsigned char c) { return std::tolower(c); }); - j[verb_name] = - GetSchema::Out{endpoint.params_schema, endpoint.result_schema}; - } - } - + auto j = get_endpoint_schema(tx, in); if (j.empty()) { return make_error( @@ -288,8 +259,7 @@ namespace ccf return make_success(j); }; - make_command_endpoint( - "api/schema", HTTP_GET, json_command_adapter(get_schema)) + make_endpoint("api/schema", HTTP_GET, json_adapter(get_schema)) .set_auto_schema() .install(); diff --git a/src/node/rpc/endpoint_registry.h b/src/node/rpc/endpoint_registry.h index b717db75dd6..f91a9d70f55 100644 --- a/src/node/rpc/endpoint_registry.h +++ b/src/node/rpc/endpoint_registry.h @@ -3,6 +3,7 @@ #pragma once #include "ds/json_schema.h" +#include "ds/openapi.h" #include "enclave/rpc_context.h" #include "http/http_consts.h" #include "http/ws_consts.h" @@ -65,6 +66,15 @@ namespace ccf Write }; + const std::string method_prefix; + + struct OpenApiInfo + { + std::string title = "Empty title"; + std::string description = "Empty description"; + std::string document_version = "0.0.1"; + } openapi_info; + struct Metrics { size_t calls = 0; @@ -72,6 +82,10 @@ namespace ccf size_t failures = 0; }; + struct Endpoint; + using SchemaBuilderFn = + std::function; + /** An Endpoint represents a user-defined resource that can be invoked by * authorised users via HTTP requests, over TLS. An Endpoint is accessible * at a specific verb and URI, e.g. POST /app/accounts or GET /app/records. @@ -88,6 +102,9 @@ namespace ccf EndpointFunction func; EndpointRegistry* registry = nullptr; Metrics metrics = {}; + + std::vector schema_builders = {}; + nlohmann::json params_schema = nullptr; /** Sets the JSON schema that the request parameters must comply with. @@ -98,6 +115,35 @@ namespace ccf Endpoint& set_params_schema(const nlohmann::json& j) { params_schema = j; + + schema_builders.push_back([]( + nlohmann::json& document, + const Endpoint& endpoint) { + const auto http_verb = endpoint.verb.get_http_method(); + if (!http_verb.has_value()) + { + return; + } + + using namespace ds::openapi; + + if (http_verb.value() == HTTP_GET || http_verb.value() == HTTP_DELETE) + { + add_query_parameters( + document, + endpoint.method, + endpoint.params_schema, + http_verb.value()); + } + else + { + auto& rb = request_body(path_operation( + ds::openapi::path(document, endpoint.method), http_verb.value())); + schema(media_type(rb, http::headervalues::contenttype::JSON)) = + endpoint.params_schema; + } + }); + return *this; } @@ -111,6 +157,29 @@ namespace ccf Endpoint& set_result_schema(const nlohmann::json& j) { result_schema = j; + + schema_builders.push_back( + [j](nlohmann::json& document, const Endpoint& endpoint) { + const auto http_verb = endpoint.verb.get_http_method(); + if (!http_verb.has_value()) + { + return; + } + + using namespace ds::openapi; + auto& r = response( + path_operation( + ds::openapi::path(document, endpoint.method), + http_verb.value()), + HTTP_STATUS_OK); + + if (endpoint.result_schema != nullptr) + { + schema(media_type(r, http::headervalues::contenttype::JSON)) = + endpoint.result_schema; + } + }); + return *this; } @@ -133,6 +202,35 @@ namespace ccf if constexpr (!std::is_same_v) { params_schema = ds::json::build_schema(method + "/params"); + + schema_builders.push_back( + [](nlohmann::json& document, const Endpoint& endpoint) { + const auto http_verb = endpoint.verb.get_http_method(); + if (!http_verb.has_value()) + { + // Non-HTTP (ie WebSockets) endpoints are not documented + return; + } + + if ( + http_verb.value() == HTTP_GET || + http_verb.value() == HTTP_DELETE) + { + add_query_parameters( + document, + endpoint.method, + endpoint.params_schema, + http_verb.value()); + } + else + { + ds::openapi::add_request_body_schema( + document, + endpoint.method, + http_verb.value(), + http::headervalues::contenttype::JSON); + } + }); } else { @@ -142,6 +240,22 @@ namespace ccf if constexpr (!std::is_same_v) { result_schema = ds::json::build_schema(method + "/result"); + + schema_builders.push_back( + [](nlohmann::json& document, const Endpoint& endpoint) { + const auto http_verb = endpoint.verb.get_http_method(); + if (!http_verb.has_value()) + { + return; + } + + ds::openapi::add_response_schema( + document, + endpoint.method, + http_verb.value(), + HTTP_STATUS_OK, + http::headervalues::contenttype::JSON); + }); } else { @@ -328,9 +442,38 @@ namespace ccf return templated; } + static void add_query_parameters( + nlohmann::json& document, + const std::string& uri, + const nlohmann::json& schema, + http_method verb) + { + if (schema["type"] != "object") + { + throw std::logic_error( + fmt::format("Unexpected params schema type: {}", schema.dump())); + } + + const auto& required_parameters = schema["required"]; + for (const auto& [name, schema] : schema["properties"].items()) + { + auto parameter = nlohmann::json::object(); + parameter["name"] = name; + parameter["in"] = "query"; + parameter["required"] = + required_parameters.find(name) != required_parameters.end(); + parameter["schema"] = schema; + ds::openapi::add_request_parameter_schema( + document, uri, verb, parameter); + } + } + public: EndpointRegistry( - kv::Store& tables, const std::string& certs_table_name = "") + const std::string& method_prefix_, + kv::Store& tables, + const std::string& certs_table_name = "") : + method_prefix(method_prefix_) { if (!certs_table_name.empty()) { @@ -436,19 +579,30 @@ namespace ccf return default_endpoint.value(); } - /** Populate out with all supported methods + static void add_endpoint_to_api_document( + nlohmann::json& document, const Endpoint& endpoint) + { + for (const auto& builder_fn : endpoint.schema_builders) + { + builder_fn(document, endpoint); + } + } + + /** Populate document with all supported methods * - * This is virtual since the default endpoint may do its own dispatch - * internally, so derived implementations must be able to populate the list - * with the supported methods however it constructs them. + * This is virtual since derived classes may do their own dispatch + * internally, so must be able to populate the document + * with the supported endpoints however it defines them. */ - virtual void list_methods(kv::Tx&, ListMethods::Out& out) + virtual void build_api(nlohmann::json& document, kv::Tx&) { + ds::openapi::server(document, fmt::format("/{}", method_prefix)); + for (const auto& [path, verb_endpoints] : fully_qualified_endpoints) { for (const auto& [verb, endpoint] : verb_endpoints) { - out.endpoints.push_back({verb.c_str(), path}); + add_endpoint_to_api_document(document, endpoint); } } @@ -456,9 +610,51 @@ namespace ccf { for (const auto& [verb, endpoint] : verb_endpoints) { - out.endpoints.push_back({verb.c_str(), path}); + add_endpoint_to_api_document(document, endpoint); + + for (const auto& name : endpoint.template_component_names) + { + auto parameter = nlohmann::json::object(); + parameter["name"] = name; + parameter["in"] = "path"; + parameter["required"] = true; + parameter["schema"] = {{"type", "string"}}; + ds::openapi::add_path_parameter_schema( + document, endpoint.method, parameter); + } + } + } + } + + virtual nlohmann::json get_endpoint_schema(kv::Tx&, const GetSchema::In& in) + { + auto j = nlohmann::json::object(); + + const auto it = fully_qualified_endpoints.find(in.method); + if (it != fully_qualified_endpoints.end()) + { + for (const auto& [verb, endpoint] : it->second) + { + std::string verb_name = verb.c_str(); + nonstd::to_lower(verb_name); + j[verb_name] = + GetSchema::Out{endpoint.params_schema, endpoint.result_schema}; } } + + const auto templated_it = templated_endpoints.find(in.method); + if (templated_it != templated_endpoints.end()) + { + for (const auto& [verb, endpoint] : templated_it->second) + { + std::string verb_name = verb.c_str(); + nonstd::to_lower(verb_name); + j[verb_name] = + GetSchema::Out{endpoint.params_schema, endpoint.result_schema}; + } + } + + return j; } virtual void endpoint_metrics(kv::Tx&, EndpointMetrics::Out& out) diff --git a/src/node/rpc/member_frontend.h b/src/node/rpc/member_frontend.h index e9f414ba436..77de81af96e 100644 --- a/src/node/rpc/member_frontend.h +++ b/src/node/rpc/member_frontend.h @@ -752,12 +752,20 @@ namespace ccf NetworkTables& network, AbstractNodeState& node, ShareManager& share_manager) : - CommonEndpointRegistry(*network.tables, Tables::MEMBER_CERT_DERS), + CommonEndpointRegistry( + get_actor_prefix(ActorsType::members), + *network.tables, + Tables::MEMBER_CERT_DERS), network(network), node(node), share_manager(share_manager), tsr(network) - {} + { + openapi_info.title = "CCF Governance API"; + openapi_info.description = + "This API is used to submit and query proposals which affect CCF's " + "public governance tables."; + } void init_handlers(kv::Store& tables_) override { diff --git a/src/node/rpc/node_frontend.h b/src/node/rpc/node_frontend.h index b9ce5f0cebd..d7eec7538fc 100644 --- a/src/node/rpc/node_frontend.h +++ b/src/node/rpc/node_frontend.h @@ -136,10 +136,16 @@ namespace ccf public: NodeEndpoints(NetworkState& network, AbstractNodeState& node) : - CommonEndpointRegistry(*network.tables), + CommonEndpointRegistry( + get_actor_prefix(ActorsType::nodes), *network.tables), network(network), node(node) - {} + { + openapi_info.title = "CCF Public Node API"; + openapi_info.description = + "This API provides public, uncredentialed access to service and node " + "state."; + } void init_handlers(kv::Store& tables_) override { diff --git a/src/node/rpc/serialization.h b/src/node/rpc/serialization.h index d641b373772..d4bdca1c51f 100644 --- a/src/node/rpc/serialization.h +++ b/src/node/rpc/serialization.h @@ -112,11 +112,6 @@ namespace ccf DECLARE_JSON_TYPE(GetUserId::In) DECLARE_JSON_REQUIRED_FIELDS(GetUserId::In, cert) - DECLARE_JSON_TYPE(ListMethods::Endpoint) - DECLARE_JSON_REQUIRED_FIELDS(ListMethods::Endpoint, verb, path) - DECLARE_JSON_TYPE(ListMethods::Out) - DECLARE_JSON_REQUIRED_FIELDS(ListMethods::Out, endpoints) - DECLARE_JSON_TYPE(EndpointMetrics::Metric) DECLARE_JSON_REQUIRED_FIELDS(EndpointMetrics::Metric, calls, errors, failures) DECLARE_JSON_TYPE(EndpointMetrics::Out) diff --git a/src/node/rpc/test/frontend_test.cpp b/src/node/rpc/test/frontend_test.cpp index dbd4f797fe0..7d61693be94 100644 --- a/src/node/rpc/test/frontend_test.cpp +++ b/src/node/rpc/test/frontend_test.cpp @@ -240,7 +240,7 @@ class TestNoCertsFrontend : public RpcFrontend public: TestNoCertsFrontend(kv::Store& tables) : RpcFrontend(tables, endpoints), - endpoints(tables) + endpoints("test", tables) { open(); diff --git a/src/node/rpc/user_frontend.h b/src/node/rpc/user_frontend.h index 615f2f04bd8..fb290014b9d 100644 --- a/src/node/rpc/user_frontend.h +++ b/src/node/rpc/user_frontend.h @@ -28,7 +28,9 @@ namespace ccf h, tables.get(Tables::USER_CLIENT_SIGNATURES)), users(tables.get(Tables::USERS)) - {} + { + h.openapi_info.title = "CCF Application API"; + } void open() override { @@ -81,11 +83,15 @@ namespace ccf { public: UserEndpointRegistry(kv::Store& store) : - CommonEndpointRegistry(store, Tables::USER_CERT_DERS) + CommonEndpointRegistry( + get_actor_prefix(ActorsType::users), store, Tables::USER_CERT_DERS) {} UserEndpointRegistry(NetworkTables& network) : - CommonEndpointRegistry(*network.tables, Tables::USER_CERT_DERS) + CommonEndpointRegistry( + get_actor_prefix(ActorsType::users), + *network.tables, + Tables::USER_CERT_DERS) {} }; diff --git a/tests/requirements.txt b/tests/requirements.txt index 6d08506e3bf..b8477ee3999 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -4,4 +4,5 @@ loguru coincurve psutil cimetrics>=0.2.1 -pynacl \ No newline at end of file +pynacl +openapi-spec-validator \ No newline at end of file diff --git a/tests/schema.py b/tests/schema.py index 387c03f8297..455dda19470 100644 --- a/tests/schema.py +++ b/tests/schema.py @@ -8,6 +8,8 @@ import infra.proc import infra.e2e_args import infra.checker +import openapi_spec_validator + from loguru import logger as LOG @@ -29,18 +31,26 @@ def run(args): for filename in filenames ) + documents_valid = True + all_methods = [] def fetch_schema(client, prefix): - list_response = client.get(f"/{prefix}/api") + api_response = client.get(f"/{prefix}/api") check( - list_response, error=lambda status, msg: status == http.HTTPStatus.OK.value + api_response, error=lambda status, msg: status == http.HTTPStatus.OK.value ) - methods = list_response.body.json()["endpoints"] - all_methods.extend([m["path"] for m in methods]) - for method in [m["path"] for m in methods]: + response_body = api_response.body.json() + paths = response_body["paths"] + all_methods.extend(paths.keys()) + + # Fetch the schema of each method + for method, _ in paths.items(): schema_found = False + expected_method_prefix = "/" + if method.startswith(expected_method_prefix): + method = method[len(expected_method_prefix) :] schema_response = client.get(f'/{prefix}/api/schema?method="{method}"') check( schema_response, @@ -84,6 +94,35 @@ def fetch_schema(client, prefix): else: methods_without_schema.add(method) + formatted_schema = json.dumps(response_body, indent=2) + openapi_target_file = os.path.join(args.schema_dir, f"{prefix}_openapi.json") + + try: + old_schema.remove(openapi_target_file) + except KeyError: + pass + + with open(openapi_target_file, "a+") as f: + f.seek(0) + previous = f.read() + if previous != formatted_schema: + LOG.debug("Writing schema to {}".format(openapi_target_file)) + f.truncate(0) + f.seek(0) + f.write(formatted_schema) + changed_files.append(openapi_target_file) + else: + LOG.debug("Schema matches in {}".format(openapi_target_file)) + + try: + openapi_spec_validator.validate_spec(response_body) + except Exception as e: + LOG.error(f"Validation of {prefix} schema failed") + LOG.error(e) + return False + + return True + with infra.network.network( hosts, args.binary_dir, args.debug_nodes, args.perf_nodes ) as network: @@ -94,20 +133,18 @@ def fetch_schema(client, prefix): with primary.client("user0") as user_client: LOG.info("user frontend") - fetch_schema(user_client, "app") + if not fetch_schema(user_client, "app"): + documents_valid = False with primary.client() as node_client: LOG.info("node frontend") - fetch_schema(node_client, "node") + if not fetch_schema(node_client, "node"): + documents_valid = False with primary.client("member0") as member_client: LOG.info("member frontend") - fetch_schema(member_client, "gov") - - if len(methods_without_schema) > 0: - LOG.info("The following methods have no schema:") - for m in sorted(methods_without_schema): - LOG.info(" " + m) + if not fetch_schema(member_client, "gov"): + documents_valid = False made_changes = False @@ -134,7 +171,7 @@ def fetch_schema(client, prefix): for method in sorted(set(all_methods)): LOG.info(f" {method}") - if made_changes: + if made_changes or not documents_valid: sys.exit(1) diff --git a/tests/suite/test_requirements.py b/tests/suite/test_requirements.py index eff753af1ac..a73516fd7a1 100644 --- a/tests/suite/test_requirements.py +++ b/tests/suite/test_requirements.py @@ -48,12 +48,20 @@ def wrapper(network, args, *nargs, **kwargs): def supports_methods(*methods): + def remove_prefix(s, prefix): + if s.startswith(prefix): + return s[len(prefix) :] + return s + def check(network, args, *nargs, **kwargs): primary, _ = network.find_primary() with primary.client("user0") as c: response = c.get("/app/api") - supported_methods = response.body.json()["endpoints"] - missing = {*methods}.difference([sm["path"] for sm in supported_methods]) + supported_methods = response.body.json()["paths"] + LOG.warning(f"Supported methods are: {supported_methods.keys()}") + missing = {*methods}.difference( + [remove_prefix(key, "/") for key in supported_methods.keys()] + ) if missing: concat = ", ".join(missing) raise TestRequirementsNotMet(f"Missing required methods: {concat}")