Produce OpenAPI document describing CCF's endpoints (#1612)

This commit is contained in:
Eddy Ashton 2020-09-25 10:16:12 +01:00 коммит произвёл GitHub
Родитель e499f62f8d
Коммит 37d78ecfe2
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
89 изменённых файлов: 4325 добавлений и 299 удалений

Просмотреть файл

@ -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

Просмотреть файл

@ -1,5 +1,4 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"signed_req": {
"properties": {

Просмотреть файл

@ -1,5 +1,4 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"state_digest": {
"items": {

Просмотреть файл

@ -1,5 +1,4 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "ack/result",
"type": "boolean"
}

Просмотреть файл

@ -1,5 +1,4 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"method": {
"type": "string"

Просмотреть файл

@ -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": [

Просмотреть файл

@ -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"
}

Просмотреть файл

@ -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"
}
]
}

Просмотреть файл

@ -1,5 +1,4 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"versions": {
"items": {

Просмотреть файл

@ -1,5 +1,4 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"seqno": {
"maximum": 9223372036854775807,

Просмотреть файл

@ -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"
},
"type": "array"
}
],
"type": "array"
"errors": {
"maximum": 18446744073709551615,
"minimum": 0,
"type": "integer"
},
"failures": {
"maximum": 18446744073709551615,
"minimum": 0,
"type": "integer"
}
},
"required": [
"calls",
"errors",
"failures"
],
"type": "object"
},
"type": "object"
},
"type": "array"
"type": "object"
}
},
"required": [

1211
doc/schemas/gov_openapi.json Normal file

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -1,5 +1,4 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"seqno": {
"maximum": 9223372036854775807,

Просмотреть файл

@ -1,5 +1,4 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"status": {
"enum": [

Просмотреть файл

@ -1,5 +1,4 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"id": {
"maximum": 18446744073709551615,

Просмотреть файл

@ -1,5 +1,4 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "log/private/admin_only/result",
"type": "boolean"
}

Просмотреть файл

@ -1,5 +1,4 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"id": {
"maximum": 18446744073709551615,

Просмотреть файл

@ -1,5 +1,4 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "log/private/anonymous/result",
"type": "boolean"
}

Просмотреть файл

@ -1,5 +1,4 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"id": {
"maximum": 18446744073709551615,

Просмотреть файл

@ -1,5 +1,4 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "log/private/result",
"type": "boolean"
}

Просмотреть файл

@ -1,5 +1,4 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"id": {
"maximum": 18446744073709551615,

Просмотреть файл

@ -1,5 +1,4 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"msg": {
"type": "string"

Просмотреть файл

@ -1,5 +1,4 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"id": {
"maximum": 18446744073709551615,

Просмотреть файл

@ -1,5 +1,4 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "log/private/result",
"type": "boolean"
}

Просмотреть файл

@ -1,5 +1,4 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"id": {
"maximum": 18446744073709551615,

Просмотреть файл

@ -1,5 +1,4 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "log/private/result",
"type": "boolean"
}

Просмотреть файл

@ -1,5 +1,4 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"id": {
"maximum": 18446744073709551615,

Просмотреть файл

@ -1,5 +1,4 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "log/public/result",
"type": "boolean"
}

Просмотреть файл

@ -1,5 +1,4 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"id": {
"type": "number"

Просмотреть файл

@ -1,5 +1,4 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"msg": {
"type": "string"

Просмотреть файл

@ -1,5 +1,4 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"id": {
"type": "number"

Просмотреть файл

@ -1,5 +1,4 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "log/public/result",
"type": "bool"
"type": "boolean"
}

Просмотреть файл

@ -1,5 +1,4 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"histogram": {
"properties": {

Просмотреть файл

@ -1,5 +1,4 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"nodes": {
"items": {

Просмотреть файл

@ -1,5 +1,4 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"host": {
"type": "string"

Просмотреть файл

@ -1,5 +1,4 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"nodes": {
"items": {

Просмотреть файл

@ -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"
}
]
}

Просмотреть файл

@ -1,5 +1,4 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"current_view": {
"maximum": 9223372036854775807,

Просмотреть файл

@ -1,5 +1,4 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"proposal_id": {
"maximum": 18446744073709551615,

Просмотреть файл

@ -1,5 +1,4 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"ballot": {
"properties": {

Просмотреть файл

@ -1,5 +1,4 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"ballot": {
"properties": {

Просмотреть файл

@ -1,5 +1,4 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"proposal_id": {
"maximum": 18446744073709551615,

Просмотреть файл

@ -1,5 +1,4 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"proposal_id": {
"maximum": 18446744073709551615,

Просмотреть файл

@ -1,5 +1,4 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"parameter": {},
"proposer": {

Просмотреть файл

@ -1,5 +1,4 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"ballot": {
"properties": {

Просмотреть файл

@ -1,5 +1,4 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"proposal_id": {
"maximum": 18446744073709551615,

Просмотреть файл

@ -1,5 +1,4 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"bytecode": {
"items": {

Просмотреть файл

@ -1,4 +0,0 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "query/result"
}

Просмотреть файл

@ -1,5 +1,4 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"quotes": {
"items": {

Просмотреть файл

@ -1,5 +1,4 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"quotes": {
"items": {

Просмотреть файл

@ -1,5 +1,4 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"key": {},
"table": {

Просмотреть файл

@ -1,4 +0,0 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "read/result"
}

Просмотреть файл

@ -1,5 +1,4 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"receipt": {
"items": {

Просмотреть файл

@ -1,5 +1,4 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"valid": {
"type": "boolean"

Просмотреть файл

@ -1,5 +1,4 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"commit": {
"maximum": 9223372036854775807,

Просмотреть файл

@ -1,5 +1,4 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"receipt": {
"items": {

Просмотреть файл

@ -1,5 +1,4 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "recovery_share/submit/params",
"type": "string"
}

Просмотреть файл

@ -1,5 +1,4 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "recovery_share/submit/result",
"type": "string"
}

Просмотреть файл

@ -1,5 +1,4 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"encrypted_recovery_share": {
"type": "string"

Просмотреть файл

@ -1,5 +1,4 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"id": {
"maximum": 18446744073709551615,

Просмотреть файл

@ -1,5 +1,4 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"seqno": {
"maximum": 9223372036854775807,

Просмотреть файл

@ -1,5 +1,4 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"status": {
"enum": [

Просмотреть файл

@ -1,5 +1,4 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"cert": {
"items": {

Просмотреть файл

@ -1,5 +1,4 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"caller_id": {
"maximum": 18446744073709551615,

Просмотреть файл

@ -655,27 +655,63 @@ namespace ccfapp
set_default(default_handler);
}
static std::pair<http_method, std::string> 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
{
out.endpoints.push_back({"POST", key});
}
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)
{
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;
}
};
#pragma clang diagnostic pop

Просмотреть файл

@ -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

Просмотреть файл

@ -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"

Просмотреть файл

@ -185,18 +185,63 @@ namespace ccfapp
set_default(json_adapter(default_handler));
}
static std::pair<http_method, std::string> 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&) {
out.endpoints.push_back({"POST", key});
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)
{
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;
}
};
class Lua : public ccf::UserRpcFrontend

Просмотреть файл

@ -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<decltype(TYPE::C_FIELD)>(); \
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<decltype(TYPE::C_FIELD)>();
#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<decltype(TYPE::FIELD)>{#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<decltype(T::foo)>();
* 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 <typename T> \
void add_schema_components_required_fields( \
T& doc, nlohmann::json& j, const TYPE& t); \
template <typename T> \
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 <typename T> \
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<BASE&>(t)), \
, \
fill_json_schema(j, static_cast<const BASE&>(t)), )
fill_json_schema(j, static_cast<const BASE&>(t)), \
, \
add_schema_components(doc, j, static_cast<const BASE&>(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<BASE&>(t)), \
from_json_optional_fields(j, t), \
fill_json_schema(j, static_cast<const BASE&>(t)), \
fill_json_schema_optional_fields(j, t))
fill_json_schema_optional_fields(j, t), \
add_schema_components(doc, j, static_cast<const BASE&>(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 <typename T> \
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 <typename T> \
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 <typename T> \
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 <typename T> \
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<TYPE, nlohmann::json> m[] = __VA_ARGS__; \

Просмотреть файл

@ -4,6 +4,8 @@
#include "ds/nonstd.h"
#define FMT_HEADER_ONLY
#include <fmt/format.h>
#include <nlohmann/json.hpp>
namespace ds
@ -36,6 +38,9 @@ namespace ds
schema["maximum"] = std::numeric_limits<T>::max();
}
template <typename T>
std::string schema_name();
template <typename T>
void fill_schema(nlohmann::json& schema);
@ -50,8 +55,23 @@ namespace ds
return element;
}
template <typename T, typename Doc>
nlohmann::json schema_element()
{
auto element = nlohmann::json::object();
fill_schema<T>(element);
return element;
}
namespace adl
{
template <typename T>
std::string schema_name()
{
T t;
return schema_name(t);
}
template <typename T>
void fill_schema(nlohmann::json& schema)
{
@ -67,6 +87,103 @@ namespace ds
}
}
template <typename T>
inline std::string schema_name()
{
if constexpr (nonstd::is_specialization<T, std::optional>::value)
{
return schema_name<typename T::value_type>();
}
else if constexpr (nonstd::is_specialization<T, std::vector>::value)
{
return fmt::format("{}_array", schema_name<typename T::value_type>());
}
else if constexpr (
nonstd::is_specialization<T, std::map>::value ||
nonstd::is_specialization<T, std::unordered_map>::value)
{
if (std::is_same<typename T::key_type, std::string>::value)
{
return fmt::format(
"named_{}", schema_name<typename T::mapped_type>());
}
else
{
return fmt::format(
"{}_to_{}",
schema_name<typename T::key_type>(),
schema_name<typename T::mapped_type>());
}
}
else if constexpr (nonstd::is_specialization<T, std::pair>::value)
{
return fmt::format(
"{}_and_{}",
schema_name<typename T::first_type>(),
schema_name<typename T::second_type>());
}
else if constexpr (std::is_same<T, std::string>::value)
{
return "string";
}
else if constexpr (std::is_same<T, bool>::value)
{
return "boolean";
}
else if constexpr (std::is_same<T, uint8_t>::value)
{
return "uint8";
}
else if constexpr (std::is_same<T, uint16_t>::value)
{
return "uint16";
}
else if constexpr (std::is_same<T, uint32_t>::value)
{
return "uint32";
}
else if constexpr (std::is_same<T, uint64_t>::value)
{
return "uint64";
}
else if constexpr (std::is_same<T, int8_t>::value)
{
return "int8";
}
else if constexpr (std::is_same<T, int16_t>::value)
{
return "int16";
}
else if constexpr (std::is_same<T, int32_t>::value)
{
return "int32";
}
else if constexpr (std::is_same<T, int64_t>::value)
{
return "int64";
}
else if constexpr (std::is_same<T, float>::value)
{
return "float";
}
else if constexpr (std::is_same<T, double>::value)
{
return "double";
}
else if constexpr (std::is_same<T, nlohmann::json>::value)
{
return "json";
}
else if constexpr (std::is_same<T, JsonSchema>::value)
{
return "json_schema";
}
else
{
return adl::schema_name<T>();
}
}
template <typename T>
inline void fill_schema(nlohmann::json& schema)
{
@ -83,18 +200,28 @@ namespace ds
nonstd::is_specialization<T, std::map>::value ||
nonstd::is_specialization<T, std::unordered_map>::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<typename T::key_type, std::string>::value)
{
items["type"] = "array";
auto sub_items = nlohmann::json::array();
sub_items.push_back(schema_element<typename T::key_type>());
sub_items.push_back(schema_element<typename T::mapped_type>());
items["items"] = sub_items;
// ...unless the keys are strings!
schema["type"] = "object";
schema["additionalProperties"] =
schema_element<typename T::mapped_type>();
}
else
{
schema["type"] = "array";
auto items = nlohmann::json::object();
{
items["type"] = "array";
auto sub_items = nlohmann::json::array();
sub_items.push_back(schema_element<typename T::key_type>());
sub_items.push_back(schema_element<typename T::mapped_type>());
items["items"] = sub_items;
}
schema["items"] = items;
}
schema["items"] = items;
}
else if constexpr (nonstd::is_specialization<T, std::pair>::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<T>::value)
{
@ -127,7 +255,7 @@ namespace ds
}
else if constexpr (std::is_same<T, JsonSchema>::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<T>(schema);

Просмотреть файл

@ -2,7 +2,9 @@
// Licensed under the Apache 2.0 License.
#pragma once
#include <algorithm>
#include <array>
#include <cctype>
#include <string>
#include <string_view>
#include <type_traits>
@ -59,4 +61,31 @@ namespace nonstd
template <class T>
using remove_cvref_t = typename remove_cvref<T>::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;
}
}

387
src/ds/openapi.h Normal file
Просмотреть файл

@ -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 <http-parser/http_parser.h>
#include <nlohmann/json.hpp>
#include <string>
#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 <typename T>
nlohmann::json add_schema_component()
{
nlohmann::json schema;
if constexpr (nonstd::is_specialization<T, std::optional>::value)
{
return add_schema_component<typename T::value_type>();
}
else if constexpr (nonstd::is_specialization<T, std::vector>::value)
{
schema["type"] = "array";
schema["items"] = add_schema_component<typename T::value_type>();
return add_schema_to_components(
document, ds::json::schema_name<T>(), schema);
}
else if constexpr (
nonstd::is_specialization<T, std::map>::value ||
nonstd::is_specialization<T, std::unordered_map>::value)
{
// Nlohmann serialises maps to an array of (K, V) pairs
if (std::is_same<typename T::key_type, std::string>::value)
{
// ...unless the keys are strings!
schema["type"] = "object";
schema["additionalProperties"] =
add_schema_component<typename T::mapped_type>();
}
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<typename T::key_type>());
sub_items.push_back(
add_schema_component<typename T::mapped_type>());
items["items"] = sub_items;
}
schema["items"] = items;
}
return add_schema_to_components(
document, ds::json::schema_name<T>(), schema);
}
else if constexpr (nonstd::is_specialization<T, std::pair>::value)
{
schema["type"] = "array";
auto items = nlohmann::json::array();
items.push_back(add_schema_component<typename T::first_type>());
items.push_back(add_schema_component<typename T::second_type>());
schema["items"] = items;
return add_schema_to_components(
document, ds::json::schema_name<T>(), schema);
}
else if constexpr (
std::is_same<T, std::string>::value || std::is_arithmetic_v<T> ||
std::is_same<T, nlohmann::json>::value ||
std::is_same<T, ds::json::JsonSchema>::value)
{
ds::json::fill_schema<T>(schema);
return add_schema_to_components(
document, ds::json::schema_name<T>(), schema);
}
else
{
const auto name = remove_invalid_chars(ds::json::schema_name<T>());
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<T>::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 <typename T>
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<T>();
if (schema_comp != nullptr)
{
schema(media_type(rb, content_type)) = sh.add_schema_component<T>();
}
}
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 <typename T>
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<T>();
if (schema_comp != nullptr)
{
schema(media_type(r, content_type)) = sh.add_schema_component<T>();
}
}
}
}
#pragma clang diagnostic pop

214
src/ds/test/openapi.cpp Normal file
Просмотреть файл

@ -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 <doctest/doctest.h>
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<Foo>(
doc, "/app/foo", HTTP_POST, http::headervalues::contenttype::JSON);
openapi::add_response_schema<size_t>(
doc,
"/app/foo",
HTTP_POST,
HTTP_STATUS_OK,
http::headervalues::contenttype::JSON);
openapi::add_response_schema<Foo>(
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<std::vector<Foo>>(
doc,
"/app/foos",
HTTP_GET,
HTTP_STATUS_OK,
http::headervalues::contenttype::JSON);
openapi::add_response_schema<std::vector<std::vector<Foo>>>(
doc,
"/app/fooss",
HTTP_GET,
HTTP_STATUS_OK,
http::headervalues::contenttype::JSON);
openapi::add_response_schema<Bar>(
doc,
"/app/bar",
HTTP_GET,
HTTP_STATUS_OK,
http::headervalues::contenttype::JSON);
openapi::add_response_schema<Baz>(
doc,
"/app/baz",
HTTP_GET,
HTTP_STATUS_OK,
http::headervalues::contenttype::JSON);
openapi::add_response_schema<std::map<std::string, Buzz>>(
doc,
"/app/buzz",
HTTP_GET,
HTTP_STATUS_OK,
http::headervalues::contenttype::JSON);
openapi::add_request_body_schema<std::optional<Bar>>(
doc, "/app/complex", HTTP_POST, http::headervalues::contenttype::JSON);
openapi::add_response_schema<std::map<Baz, std::vector<Buzz>>>(
doc,
"/app/complex",
HTTP_POST,
HTTP_STATUS_OK,
http::headervalues::contenttype::JSON);
required_doc_elements(doc);
}

Просмотреть файл

@ -35,6 +35,16 @@ namespace ccf
RESTVerb(const http_method& hm) : verb(hm) {}
RESTVerb(const ws::Verb& wv) : verb(wv) {}
std::optional<http_method> get_http_method() const
{
if (verb == ws::WEBSOCKET)
{
return std::nullopt;
}
return static_cast<http_method>(verb);
}
const char* c_str() const
{
if (verb == ws::WEBSOCKET)

Просмотреть файл

@ -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;
}

Просмотреть файл

@ -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);
}

Просмотреть файл

@ -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())
{

Просмотреть файл

@ -24,9 +24,7 @@ std::vector<uint8_t> 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;
}

Просмотреть файл

@ -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<Endpoint> endpoints;
};
using Out = nlohmann::json;
};
struct EndpointMetrics

Просмотреть файл

@ -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<Nodes>(Tables::NODES)),
node_code_ids(store.get<CodeIDs>(Tables::NODE_CODE_IDS)),
tables(&store)
@ -221,14 +223,16 @@ namespace ccf
.set_auto_schema<GetNodesByRPCAddress::In, GetNodesByRPCAddress::Out>()
.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<void, ListMethods::Out>()
make_endpoint("api", HTTP_GET, json_adapter(openapi))
.set_auto_schema<void, GetAPI::Out>()
.install();
auto endpoint_metrics_fn = [this](kv::Tx& tx, nlohmann::json&&) {
@ -242,43 +246,10 @@ namespace ccf
.set_auto_schema<void, EndpointMetrics::Out>()
.install();
auto get_schema = [this](auto&, nlohmann::json&& params) {
auto get_schema = [this](kv::Tx& tx, nlohmann::json&& params) {
const auto in = params.get<GetSchema::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();
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<GetSchema>()
.install();

Просмотреть файл

@ -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<void(nlohmann::json&, const Endpoint&)>;
/** 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<SchemaBuilderFn> 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<In, void>)
{
params_schema = ds::json::build_schema<In>(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<In>(
document,
endpoint.method,
http_verb.value(),
http::headervalues::contenttype::JSON);
}
});
}
else
{
@ -142,6 +240,22 @@ namespace ccf
if constexpr (!std::is_same_v<Out, void>)
{
result_schema = ds::json::build_schema<Out>(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<Out>(
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
*
* 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.
*/
virtual void list_methods(kv::Tx&, ListMethods::Out& out)
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 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 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,11 +610,53 @@ 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)
{
for (const auto& [path, verb_endpoints] : fully_qualified_endpoints)

Просмотреть файл

@ -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
{

Просмотреть файл

@ -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
{

Просмотреть файл

@ -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)

Просмотреть файл

@ -240,7 +240,7 @@ class TestNoCertsFrontend : public RpcFrontend
public:
TestNoCertsFrontend(kv::Store& tables) :
RpcFrontend(tables, endpoints),
endpoints(tables)
endpoints("test", tables)
{
open();

Просмотреть файл

@ -28,7 +28,9 @@ namespace ccf
h,
tables.get<ClientSignatures>(Tables::USER_CLIENT_SIGNATURES)),
users(tables.get<Users>(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)
{}
};

Просмотреть файл

@ -4,4 +4,5 @@ loguru
coincurve
psutil
cimetrics>=0.2.1
pynacl
pynacl
openapi-spec-validator

Просмотреть файл

@ -8,6 +8,8 @@ import infra.network
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 run(args):
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 run(args):
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 run(args):
for method in sorted(set(all_methods)):
LOG.info(f" {method}")
if made_changes:
if made_changes or not documents_valid:
sys.exit(1)

Просмотреть файл

@ -48,12 +48,20 @@ def ensure_reqs(check_reqs):
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}")