diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 05d5f39..737752e 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -11,8 +11,8 @@ jobs: name: OTP ${{matrix.otp_vsn}} strategy: matrix: - otp_vsn: ['27', '26', '25'] - rebar_vsn: ['3.23.0'] + otp_vsn: ['27'] + rebar_vsn: ['3.24.0'] runs-on: 'ubuntu-24.04' env: OTPVER: ${{ matrix.otp_vsn }} diff --git a/.openapi-generator-ignore b/.openapi-generator-ignore new file mode 100644 index 0000000..b43bf86 --- /dev/null +++ b/.openapi-generator-ignore @@ -0,0 +1 @@ +README.md diff --git a/Makefile b/Makefile index b09fb29..ab2e479 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,7 @@ OutputDir ?= tmp define openapi-generator docker run --rm -v "${PWD}:/local" -w "/local" \ - openapitools/openapi-generator-cli:v4.3.1 + openapitools/openapi-generator-cli:v7.9.0 endef all: compile diff --git a/openapi.yaml b/openapi.yaml index 7792102..f3a42a0 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -1,15 +1,18 @@ openapi: 3.0.1 info: - title: API-DOCS - version: 1.0.0 + title: AMOC API + version: 1.0.1 + description: | + AMOC REST API servers: - url: / paths: /nodes: get: + operationId: getClusteredNodes tags: - status - description: List all AMOC nodes in a cluster. + summary: List all AMOC nodes in a cluster. responses: '200': description: response object @@ -19,9 +22,10 @@ paths: $ref: '#/components/schemas/NodesKV' /scenarios: get: + operationId: getAvailableScenarios tags: - scenarios - description: List all available scenarios + summary: List all available scenarios responses: '200': description: response object @@ -31,9 +35,10 @@ paths: $ref: '#/components/schemas/ScenarioList' '/scenarios/info/{id}': get: + operationId: getScenarioDescription tags: - scenarios - description: Get scenario description + summary: Get scenario description parameters: - name: id in: path @@ -52,9 +57,10 @@ paths: description: no scenario with such a name '/scenarios/defaults/{id}': get: + operationId: getScenarioSettings tags: - scenarios - description: Get scenario default settings + summary: Get scenario default settings parameters: - name: id in: path @@ -73,9 +79,10 @@ paths: description: no scenario with such a name /status: get: + operationId: getAmocAppStatus tags: - status - description: Get AMOC app status + summary: Get AMOC app status responses: '200': description: response object @@ -85,9 +92,10 @@ paths: $ref: '#/components/schemas/AmocStatus' '/status/{node}': get: + operationId: getAmocAppStatusOnNode tags: - status - description: Get AMOC app status on a remote node + summary: Get AMOC app status on a remote node parameters: - name: node in: path @@ -106,8 +114,10 @@ paths: description: no node with such a name /scenarios/upload: put: + operationId: uploadNewScenario tags: - scenarios + summary: Upload a new scenario. description: >- Uploads new scenario, you can run the next command to upload a file using curl utility @@ -139,9 +149,10 @@ paths: $ref: '#/components/schemas/Error' /execution/start: patch: + operationId: startScenario tags: - execution - description: Start scenario + summary: Start scenario requestBody: description: request body (as json) content: @@ -164,9 +175,14 @@ paths: $ref: '#/components/schemas/Error' /execution/stop: patch: + operationId: stopScenario tags: - execution - description: Stop scenario + summary: Stop scenario + requestBody: + description: request body (as json) + content: + application/json: {} responses: '200': description: execution stopped successfully @@ -180,9 +196,10 @@ paths: $ref: '#/components/schemas/Error' /execution/add_users: patch: + operationId: addUsers tags: - execution - description: add new users + summary: add new users requestBody: description: request body (as json) content: @@ -203,9 +220,10 @@ paths: $ref: '#/components/schemas/Error' /execution/remove_users: patch: + operationId: removeUsers tags: - execution - description: stop users + summary: stop users requestBody: description: request body (as json) content: @@ -226,9 +244,10 @@ paths: $ref: '#/components/schemas/Error' /execution/update_settings: patch: + operationId: updateSettings tags: - execution - description: update scenario settings + summary: update scenario settings requestBody: description: request body (as json) content: diff --git a/priv/openapi.json b/priv/openapi.json index 914fcf0..98af55d 100644 --- a/priv/openapi.json +++ b/priv/openapi.json @@ -1,8 +1,9 @@ { "openapi" : "3.0.1", "info" : { - "title" : "API-DOCS", - "version" : "1.0.0" + "description" : "AMOC REST API\n", + "title" : "AMOC API", + "version" : "1.0.1" }, "servers" : [ { "url" : "/" @@ -10,7 +11,7 @@ "paths" : { "/nodes" : { "get" : { - "description" : "List all AMOC nodes in a cluster.", + "operationId" : "getClusteredNodes", "responses" : { "200" : { "content" : { @@ -23,12 +24,13 @@ "description" : "response object" } }, + "summary" : "List all AMOC nodes in a cluster.", "tags" : [ "status" ] } }, "/scenarios" : { "get" : { - "description" : "List all available scenarios", + "operationId" : "getAvailableScenarios", "responses" : { "200" : { "content" : { @@ -41,12 +43,13 @@ "description" : "response object" } }, + "summary" : "List all available scenarios", "tags" : [ "scenarios" ] } }, "/scenarios/info/{id}" : { "get" : { - "description" : "Get scenario description", + "operationId" : "getScenarioDescription", "parameters" : [ { "description" : "Scenario name", "explode" : false, @@ -73,12 +76,13 @@ "description" : "no scenario with such a name" } }, + "summary" : "Get scenario description", "tags" : [ "scenarios" ] } }, "/scenarios/defaults/{id}" : { "get" : { - "description" : "Get scenario default settings", + "operationId" : "getScenarioSettings", "parameters" : [ { "description" : "Scenario name", "explode" : false, @@ -105,12 +109,13 @@ "description" : "no scenario with such a name" } }, + "summary" : "Get scenario default settings", "tags" : [ "scenarios" ] } }, "/status" : { "get" : { - "description" : "Get AMOC app status", + "operationId" : "getAmocAppStatus", "responses" : { "200" : { "content" : { @@ -123,12 +128,13 @@ "description" : "response object" } }, + "summary" : "Get AMOC app status", "tags" : [ "status" ] } }, "/status/{node}" : { "get" : { - "description" : "Get AMOC app status on a remote node", + "operationId" : "getAmocAppStatusOnNode", "parameters" : [ { "description" : "Node name", "explode" : false, @@ -155,12 +161,14 @@ "description" : "no node with such a name" } }, + "summary" : "Get AMOC app status on a remote node", "tags" : [ "status" ] } }, "/scenarios/upload" : { "put" : { "description" : "Uploads new scenario, you can run the next command to upload a file using curl utility\n\n`curl -s -H \"Content-Type: text/plain\" -T http:///upload`", + "operationId" : "uploadNewScenario", "requestBody" : { "content" : { "application/octet-stream" : { @@ -198,12 +206,13 @@ "description" : "response object" } }, + "summary" : "Upload a new scenario.", "tags" : [ "scenarios" ] } }, "/execution/start" : { "patch" : { - "description" : "Start scenario", + "operationId" : "startScenario", "requestBody" : { "content" : { "application/json" : { @@ -233,12 +242,19 @@ "description" : "internal error occurred" } }, + "summary" : "Start scenario", "tags" : [ "execution" ] } }, "/execution/stop" : { "patch" : { - "description" : "Stop scenario", + "operationId" : "stopScenario", + "requestBody" : { + "content" : { + "application/json" : { } + }, + "description" : "request body (as json)" + }, "responses" : { "200" : { "description" : "execution stopped successfully" @@ -257,12 +273,13 @@ "description" : "internal error occurred" } }, + "summary" : "Stop scenario", "tags" : [ "execution" ] } }, "/execution/add_users" : { "patch" : { - "description" : "add new users", + "operationId" : "addUsers", "requestBody" : { "content" : { "application/json" : { @@ -292,12 +309,13 @@ "description" : "internal error occurred" } }, + "summary" : "add new users", "tags" : [ "execution" ] } }, "/execution/remove_users" : { "patch" : { - "description" : "stop users", + "operationId" : "removeUsers", "requestBody" : { "content" : { "application/json" : { @@ -327,12 +345,13 @@ "description" : "internal error occurred" } }, + "summary" : "stop users", "tags" : [ "execution" ] } }, "/execution/update_settings" : { "patch" : { - "description" : "update scenario settings", + "operationId" : "updateSettings", "requestBody" : { "content" : { "application/json" : { @@ -362,6 +381,7 @@ "description" : "internal error occurred" } }, + "summary" : "update scenario settings", "tags" : [ "execution" ] } } @@ -434,7 +454,7 @@ }, "parameters" : { "additionalProperties" : { - "$ref" : "#/components/schemas/ScenarioInfo_parameters" + "$ref" : "#/components/schemas/ScenarioInfo_parameters_value" }, "example" : { "configuration_parameter_name1" : { @@ -608,6 +628,9 @@ "type" : "object" }, "Error" : { + "example" : { + "error" : "error description" + }, "properties" : { "error" : { "example" : "error description", @@ -728,7 +751,7 @@ "pattern" : "^[^@]+@[^@]+$", "type" : "string" }, - "ScenarioInfo_parameters" : { + "ScenarioInfo_parameters_value" : { "properties" : { "module" : { "type" : "string" @@ -746,7 +769,8 @@ "type" : "string" } }, - "required" : [ "default_value", "description", "module", "update_fn", "verification_fn" ] + "required" : [ "default_value", "description", "module", "update_fn", "verification_fn" ], + "type" : "object" } } } diff --git a/rebar.config b/rebar.config index 4d13e08..3e8842e 100644 --- a/rebar.config +++ b/rebar.config @@ -1,18 +1,15 @@ +{minimum_otp_vsn, "27"}. + {deps, [ - {ranch, "2.1.0"}, {cowboy, "2.12.0"}, - {jsx, "3.1.0"}, - {jesse, "1.8.0"} + {ranch, "2.1.0"}, + {jesse, "1.8.1"} ]}. -{dialyzer, [ - {warnings, [unknown]}, - {plt_extra_apps, [cowboy, cowlib, ranch]}]}. - -{xref_checks, [ - undefined_function_calls, - undefined_functions, - locals_not_used, - deprecated_function_calls, - deprecated_functions +{dialyzer, + [{plt_extra_apps, [cowboy, cowlib, ranch, jesse]}, + {warnings, [no_match, no_unused, missing_return, unknown]} ]}. + +{xref_checks, + [undefined_function_calls, deprecated_function_calls, deprecated_functions]}. diff --git a/rebar.lock b/rebar.lock index db1efac..ef9ceb7 100644 --- a/rebar.lock +++ b/rebar.lock @@ -1,23 +1,17 @@ {"1.2.0", [{<<"cowboy">>,{pkg,<<"cowboy">>,<<"2.12.0">>},0}, {<<"cowlib">>,{pkg,<<"cowlib">>,<<"2.13.0">>},1}, - {<<"jesse">>,{pkg,<<"jesse">>,<<"1.8.0">>},0}, - {<<"jsx">>,{pkg,<<"jsx">>,<<"3.1.0">>},0}, - {<<"ranch">>,{pkg,<<"ranch">>,<<"2.1.0">>},0}, - {<<"rfc3339">>,{pkg,<<"rfc3339">>,<<"0.9.0">>},1}]}. + {<<"jesse">>,{pkg,<<"jesse">>,<<"1.8.1">>},0}, + {<<"ranch">>,{pkg,<<"ranch">>,<<"2.1.0">>},0}]}. [ {pkg_hash,[ {<<"cowboy">>, <<"F276D521A1FF88B2B9B4C54D0E753DA6C66DD7BE6C9FCA3D9418B561828A3731">>}, {<<"cowlib">>, <<"DB8F7505D8332D98EF50A3EF34B34C1AFDDEC7506E4EE4DD4A3A266285D282CA">>}, - {<<"jesse">>, <<"CF7615C3F2BE892F77BCCF736F23B4BD54A0FC686C7040431AEBA5EF7932CC4D">>}, - {<<"jsx">>, <<"D12516BAA0BB23A59BB35DCCAF02A1BD08243FCBB9EFE24F2D9D056CCFF71268">>}, - {<<"ranch">>, <<"2261F9ED9574DCFCC444106B9F6DA155E6E540B2F82BA3D42B339B93673B72A3">>}, - {<<"rfc3339">>, <<"2075653DC9407541C84B1E15F8BDA2ABE95FB17C9694025E079583F2D19C1060">>}]}, + {<<"jesse">>, <<"C9E3670C7EE40F719734E3BC716578143AABA93FC7525A02A7D5CB300B3AD71E">>}, + {<<"ranch">>, <<"2261F9ED9574DCFCC444106B9F6DA155E6E540B2F82BA3D42B339B93673B72A3">>}]}, {pkg_hash_ext,[ {<<"cowboy">>, <<"8A7ABE6D183372CEB21CAA2709BEC928AB2B72E18A3911AA1771639BEF82651E">>}, {<<"cowlib">>, <<"E1E1284DC3FC030A64B1AD0D8382AE7E99DA46C3246B815318A4B848873800A4">>}, - {<<"jesse">>, <<"860EF4621DDBFB72792668929BE127E45E8B07CF19EEA264B0A9D48D36CCA41B">>}, - {<<"jsx">>, <<"0C5CC8FDC11B53CC25CF65AC6705AD39E54ECC56D1C22E4ADB8F5A53FB9427F3">>}, - {<<"ranch">>, <<"244EE3FA2A6175270D8E1FC59024FD9DBC76294A321057DE8F803B1479E76916">>}, - {<<"rfc3339">>, <<"182314DE35C9F4180B22EB5F22916D8D7A799C1109A060C752970273A9332AD6">>}]} + {<<"jesse">>, <<"0EDED3F18623FDA2F25989804A06CF518B4ACF2E9365B18C8E8C013D7E3C906F">>}, + {<<"ranch">>, <<"244EE3FA2A6175270D8E1FC59024FD9DBC76294A321057DE8F803B1479E76916">>}]} ]. diff --git a/src/amoc_rest.app.src b/src/amoc_rest.app.src index b6f6208..3e36121 100644 --- a/src/amoc_rest.app.src +++ b/src/amoc_rest.app.src @@ -1,19 +1,11 @@ -{application, amoc_rest, [ - {description, "Amoc REST API (generated by Openapi Generator https://github.com/openapitools/openapi-generator)"}, - {vsn, git}, - {registered, []}, - {applications, [ - kernel, - stdlib, - ssl, - inets, - jsx, - jesse, - cowboy - ]}, - {env, [ - ]}, - {modules, []}, - {licenses, []}, - {links, []} -]}. +{application, + amoc_rest, + [{description, + "AMOC REST API "}, + {vsn, "1.0.0"}, + {registered, []}, + {applications, [kernel, stdlib, public_key, ssl, inets, ranch, cowboy, jesse]}, + {env, []}, + {modules, []}, + {licenses, []}, + {links, []}]}. diff --git a/src/amoc_rest_api.erl b/src/amoc_rest_api.erl index cd58701..1d07021 100644 --- a/src/amoc_rest_api.erl +++ b/src/amoc_rest_api.erl @@ -1,417 +1,443 @@ -module(amoc_rest_api). +-moduledoc """ +This module offers an API for JSON schema validation, using `jesse` under the hood. + +If validation is desired, a jesse state can be loaded using `prepare_validator/1`, +and request and response can be validated using `populate_request/3` +and `validate_response/4` respectively. + +For example, the user-defined `Module:accept_callback/4` can be implemented as follows: +``` +-spec accept_callback( + Class :: openapi_api:class(), + OperationID :: openapi_api:operation_id(), + Req :: cowboy_req:req(), + Context :: openapi_logic_handler:context()) -> + {openapi_logic_handler:accept_callback_return(), + cowboy_req:req(), + openapi_logic_handler:context()}. +accept_callback(Class, OperationID, Req0, Context0) -> + ValidatorState = openapi_api:prepare_validator(), + case openapi_api:populate_request(OperationID, Req0, ValidatorState) of + {ok, Model, Req1} -> + Context1 = maps:merge(Context0, Model), + case do_accept_callback(Class, OperationID, Req1, Context1) of + {false, Req2, Context2} -> + {false, Req2, Context2}; + {{true, Code, Body}, Req2, Context2} -> + case validate_response(OperationID, Code, Body, ValidatorState) of + ok -> + process_response({ok, Code, Body}, Req2, Context2); + {error, Reason} -> + process_response({error, Reason}, Req2, Context2) + end + end; + {error, Reason, Req1} -> + process_response({error, Reason}, Req1, Context0) + end. +``` +""". + +-export([prepare_validator/0, prepare_validator/1, prepare_validator/2]). +-export([populate_request/3, validate_response/4]). + +-ignore_xref([populate_request/3, validate_response/4]). +-ignore_xref([prepare_validator/0, prepare_validator/1, prepare_validator/2]). + +-type class() :: + 'execution' + | 'scenarios' + | 'status'. + + +-type operation_id() :: + 'addUsers' | %% add new users + 'removeUsers' | %% stop users + 'startScenario' | %% Start scenario + 'stopScenario' | %% Stop scenario + 'updateSettings' | %% update scenario settings + 'getAvailableScenarios' | %% List all available scenarios + 'getScenarioDescription' | %% Get scenario description + 'getScenarioSettings' | %% Get scenario default settings + 'uploadNewScenario' | %% Upload a new scenario. + 'getAmocAppStatus' | %% Get AMOC app status + 'getAmocAppStatusOnNode' | %% Get AMOC app status on a remote node + 'getClusteredNodes' | %% List all AMOC nodes in a cluster. + {error, unknown_operation}. --export([request_params/1]). --export([request_param_info/2]). --export([populate_request/3]). --export([validate_response/4]). -%% exported to silence openapi complains --export([get_value/3, validate_response_body/4]). - --type operation_id() :: atom(). -type request_param() :: atom(). --export_type([operation_id/0]). +-export_type([class/0, operation_id/0]). --spec request_params(OperationID :: operation_id()) -> [Param :: request_param()]. +-dialyzer({nowarn_function, [validate_response_body/4]}). + +-type rule() :: + {type, binary} | + {type, byte} | + {type, integer} | + {type, float} | + {type, boolean} | + {type, date} | + {type, datetime} | + {enum, [atom()]} | + {max, Max :: number()} | + {exclusive_max, Max :: number()} | + {min, Min :: number()} | + {exclusive_min, Min :: number()} | + {max_length, MaxLength :: integer()} | + {min_length, MaxLength :: integer()} | + {pattern, Pattern :: string()} | + {schema, object | list, binary()} | + schema | + required | + not_required. +-doc #{equiv => prepare_validator/2}. +-spec prepare_validator() -> jesse_state:state(). +prepare_validator() -> + prepare_validator(<<"http://json-schema.org/draft-06/schema#">>). + +-doc #{equiv => prepare_validator/2}. +-spec prepare_validator(binary()) -> jesse_state:state(). +prepare_validator(SchemaVer) -> + prepare_validator(get_openapi_path(), SchemaVer). + +-doc """ +Loads the JSON schema and the desired validation draft into a `t:jesse_state:state()`. +""". +-spec prepare_validator(file:name_all(), binary()) -> jesse_state:state(). +prepare_validator(OpenApiPath, SchemaVer) -> + {ok, FileContents} = file:read_file(OpenApiPath), + R = json:decode(FileContents), + jesse_state:new(R, [{default_schema_ver, SchemaVer}]). + +-doc """ +Automatically loads the entire body from the cowboy req +and validates the JSON body against the schema. +""". +-spec populate_request( + OperationID :: operation_id(), + Req :: cowboy_req:req(), + ValidatorState :: jesse_state:state()) -> + {ok, Model :: #{}, Req :: cowboy_req:req()} | + {error, Reason :: any(), Req :: cowboy_req:req()}. +populate_request(OperationID, Req, ValidatorState) -> + Params = request_params(OperationID), + populate_request_params(OperationID, Params, Req, ValidatorState, #{}). -request_params('ExecutionAddUsersPatch') -> +-doc """ +Validates that the provided `Code` and `Body` comply with the `ValidatorState` schema +for the `OperationID` operation. +""". +-spec validate_response( + OperationID :: operation_id(), + Code :: 200..599, + Body :: jesse:json_term(), + ValidatorState :: jesse_state:state()) -> + ok | {ok, term()} | [ok | {ok, term()}] | no_return(). +validate_response('addUsers', 200, Body, ValidatorState) -> + validate_response_body('', '', Body, ValidatorState); +validate_response('addUsers', 409, Body, ValidatorState) -> + validate_response_body('', '', Body, ValidatorState); +validate_response('addUsers', 500, Body, ValidatorState) -> + validate_response_body('Error', 'Error', Body, ValidatorState); +validate_response('removeUsers', 200, Body, ValidatorState) -> + validate_response_body('', '', Body, ValidatorState); +validate_response('removeUsers', 409, Body, ValidatorState) -> + validate_response_body('', '', Body, ValidatorState); +validate_response('removeUsers', 500, Body, ValidatorState) -> + validate_response_body('Error', 'Error', Body, ValidatorState); +validate_response('startScenario', 200, Body, ValidatorState) -> + validate_response_body('', '', Body, ValidatorState); +validate_response('startScenario', 409, Body, ValidatorState) -> + validate_response_body('', '', Body, ValidatorState); +validate_response('startScenario', 500, Body, ValidatorState) -> + validate_response_body('Error', 'Error', Body, ValidatorState); +validate_response('stopScenario', 200, Body, ValidatorState) -> + validate_response_body('', '', Body, ValidatorState); +validate_response('stopScenario', 409, Body, ValidatorState) -> + validate_response_body('', '', Body, ValidatorState); +validate_response('stopScenario', 500, Body, ValidatorState) -> + validate_response_body('Error', 'Error', Body, ValidatorState); +validate_response('updateSettings', 200, Body, ValidatorState) -> + validate_response_body('', '', Body, ValidatorState); +validate_response('updateSettings', 409, Body, ValidatorState) -> + validate_response_body('', '', Body, ValidatorState); +validate_response('updateSettings', 500, Body, ValidatorState) -> + validate_response_body('Error', 'Error', Body, ValidatorState); +validate_response('getAvailableScenarios', 200, Body, ValidatorState) -> + validate_response_body('ScenarioList', 'ScenarioList', Body, ValidatorState); +validate_response('getScenarioDescription', 200, Body, ValidatorState) -> + validate_response_body('ScenarioInfo', 'ScenarioInfo', Body, ValidatorState); +validate_response('getScenarioDescription', 404, Body, ValidatorState) -> + validate_response_body('', '', Body, ValidatorState); +validate_response('getScenarioSettings', 200, Body, ValidatorState) -> + validate_response_body('ScenarioDefaults', 'ScenarioDefaults', Body, ValidatorState); +validate_response('getScenarioSettings', 404, Body, ValidatorState) -> + validate_response_body('', '', Body, ValidatorState); +validate_response('uploadNewScenario', 200, Body, ValidatorState) -> + validate_response_body('UploadResp', 'UploadResp', Body, ValidatorState); +validate_response('uploadNewScenario', 400, Body, ValidatorState) -> + validate_response_body('Error', 'Error', Body, ValidatorState); +validate_response('getAmocAppStatus', 200, Body, ValidatorState) -> + validate_response_body('AmocStatus', 'AmocStatus', Body, ValidatorState); +validate_response('getAmocAppStatusOnNode', 200, Body, ValidatorState) -> + validate_response_body('AmocStatus', 'AmocStatus', Body, ValidatorState); +validate_response('getAmocAppStatusOnNode', 404, Body, ValidatorState) -> + validate_response_body('', '', Body, ValidatorState); +validate_response('getClusteredNodes', 200, Body, ValidatorState) -> + validate_response_body('NodesKV', 'NodesKV', Body, ValidatorState); +validate_response(_OperationID, _Code, _Body, _ValidatorState) -> + ok. + +%%% +-spec request_params(OperationID :: operation_id()) -> [Param :: request_param()]. +request_params('addUsers') -> [ 'ExecutionChangeUsers' ]; - -request_params('ExecutionRemoveUsersPatch') -> +request_params('removeUsers') -> [ 'ExecutionChangeUsers' ]; - -request_params('ExecutionStartPatch') -> +request_params('startScenario') -> [ 'ExecutionStart' ]; - -request_params('ExecutionStopPatch') -> +request_params('stopScenario') -> [ ]; - -request_params('ExecutionUpdateSettingsPatch') -> +request_params('updateSettings') -> [ 'ExecutionUpdateSettings' ]; - - -request_params('ScenariosDefaultsIdGet') -> +request_params('getAvailableScenarios') -> [ - 'id' ]; - -request_params('ScenariosGet') -> +request_params('getScenarioDescription') -> [ + 'id' ]; - -request_params('ScenariosInfoIdGet') -> +request_params('getScenarioSettings') -> [ 'id' ]; - -request_params('ScenariosUploadPut') -> +request_params('uploadNewScenario') -> [ + 'file' ]; - - -request_params('NodesGet') -> +request_params('getAmocAppStatus') -> [ ]; - -request_params('StatusGet') -> +request_params('getAmocAppStatusOnNode') -> [ + 'node' ]; - -request_params('StatusNodeGet') -> +request_params('getClusteredNodes') -> [ - 'node' ]; - request_params(_) -> error(unknown_operation). --type rule() :: - {type, 'binary'} | - {type, 'integer'} | - {type, 'float'} | - {type, 'binary'} | - {type, 'boolean'} | - {type, 'date'} | - {type, 'datetime'} | - {enum, [atom()]} | - {max, Max :: number()} | - {exclusive_max, Max :: number()} | - {min, Min :: number()} | - {exclusive_min, Min :: number()} | - {max_length, MaxLength :: integer()} | - {min_length, MaxLength :: integer()} | - {pattern, Pattern :: string()} | - schema | - required | - not_required. - --spec request_param_info(OperationID :: operation_id(), Name :: request_param()) -> #{ - source => qs_val | binding | header | body, - rules => [rule()] -}. - - - -request_param_info('ExecutionAddUsersPatch', 'ExecutionChangeUsers') -> +-spec request_param_info(OperationID :: operation_id(), Name :: request_param()) -> + #{source => qs_val | binding | header | body, rules => [rule()]}. +request_param_info('addUsers', 'ExecutionChangeUsers') -> #{ - source => body, + source => body, rules => [ - schema, + {schema, object, <<"#/components/schemas/ExecutionChangeUsers">>}, required ] }; - -request_param_info('ExecutionRemoveUsersPatch', 'ExecutionChangeUsers') -> +request_param_info('removeUsers', 'ExecutionChangeUsers') -> #{ - source => body, + source => body, rules => [ - schema, + {schema, object, <<"#/components/schemas/ExecutionChangeUsers">>}, required ] }; - -request_param_info('ExecutionStartPatch', 'ExecutionStart') -> +request_param_info('startScenario', 'ExecutionStart') -> #{ - source => body, + source => body, rules => [ - schema, + {schema, object, <<"#/components/schemas/ExecutionStart">>}, required ] }; - -request_param_info('ExecutionUpdateSettingsPatch', 'ExecutionUpdateSettings') -> +request_param_info('updateSettings', 'ExecutionUpdateSettings') -> #{ - source => body, + source => body, rules => [ - schema, + {schema, object, <<"#/components/schemas/ExecutionUpdateSettings">>}, required ] }; - - -request_param_info('ScenariosDefaultsIdGet', 'id') -> +request_param_info('getScenarioDescription', 'id') -> #{ - source => binding , + source => binding, rules => [ - {type, 'binary'}, + {type, binary}, required ] }; - -request_param_info('ScenariosInfoIdGet', 'id') -> +request_param_info('getScenarioSettings', 'id') -> #{ - source => binding , + source => binding, rules => [ - {type, 'binary'}, + {type, binary}, required ] }; - - -request_param_info('StatusNodeGet', 'node') -> +request_param_info('uploadNewScenario', 'file') -> #{ - source => binding , + source => body, rules => [ - {type, 'binary'}, - {pattern, "^[^@]+@[^@]+$" }, + {type, binary}, + not_required + ] + }; +request_param_info('getAmocAppStatusOnNode', 'node') -> + #{ + source => binding, + rules => [ + {type, binary}, + {pattern, "^[^@]+@[^@]+$"}, required ] }; - request_param_info(OperationID, Name) -> error({unknown_param, OperationID, Name}). --spec populate_request( - OperationID :: operation_id(), - Req :: cowboy_req:req(), - ValidatorState :: jesse_state:state() -) -> - {ok, Model :: #{}, Req :: cowboy_req:req()} | - {error, Reason :: any(), Req :: cowboy_req:req()}. - -populate_request(OperationID, Req, ValidatorState) -> - Params = request_params(OperationID), - populate_request_params(OperationID, Params, Req, ValidatorState, #{}). - +-spec populate_request_params( + operation_id(), [request_param()], cowboy_req:req(), jesse_state:state(), map()) -> + {ok, map(), cowboy_req:req()} | {error, _, cowboy_req:req()}. populate_request_params(_, [], Req, _, Model) -> {ok, Model, Req}; - -populate_request_params(OperationID, [FieldParams | T], Req0, ValidatorState, Model) -> - case populate_request_param(OperationID, FieldParams, Req0, ValidatorState) of - {ok, K, V, Req} -> - populate_request_params(OperationID, T, Req, ValidatorState, maps:put(K, V, Model)); +populate_request_params(OperationID, [ReqParamName | T], Req0, ValidatorState, Model0) -> + case populate_request_param(OperationID, ReqParamName, Req0, ValidatorState) of + {ok, V, Req} -> + Model = maps:put(ReqParamName, V, Model0), + populate_request_params(OperationID, T, Req, ValidatorState, Model); Error -> Error end. -populate_request_param(OperationID, Name, Req0, ValidatorState) -> - #{rules := Rules, source := Source} = request_param_info(OperationID, Name), - case get_value(Source, Name, Req0) of +-spec populate_request_param( + operation_id(), request_param(), cowboy_req:req(), jesse_state:state()) -> + {ok, term(), cowboy_req:req()} | {error, term(), cowboy_req:req()}. +populate_request_param(OperationID, ReqParamName, Req0, ValidatorState) -> + #{rules := Rules, source := Source} = request_param_info(OperationID, ReqParamName), + case get_value(Source, ReqParamName, Req0) of {error, Reason, Req} -> {error, Reason, Req}; {Value, Req} -> - case prepare_param(Rules, Name, Value, ValidatorState) of - {ok, Result} -> {ok, Name, Result, Req}; + case prepare_param(Rules, ReqParamName, Value, ValidatorState) of + {ok, Result} -> {ok, Result, Req}; {error, Reason} -> {error, Reason, Req} end end. --spec validate_response( - OperationID :: operation_id(), - Code :: 200..599, - Body :: jesse:json_term(), - ValidatorState :: jesse_state:state() -) -> ok | no_return(). - - -validate_response('ExecutionAddUsersPatch', 200, Body, ValidatorState) -> - validate_response_body('', '', Body, ValidatorState); -validate_response('ExecutionAddUsersPatch', 409, Body, ValidatorState) -> - validate_response_body('', '', Body, ValidatorState); -validate_response('ExecutionAddUsersPatch', 500, Body, ValidatorState) -> - validate_response_body('Error', 'Error', Body, ValidatorState); - -validate_response('ExecutionRemoveUsersPatch', 200, Body, ValidatorState) -> - validate_response_body('', '', Body, ValidatorState); -validate_response('ExecutionRemoveUsersPatch', 409, Body, ValidatorState) -> - validate_response_body('', '', Body, ValidatorState); -validate_response('ExecutionRemoveUsersPatch', 500, Body, ValidatorState) -> - validate_response_body('Error', 'Error', Body, ValidatorState); - -validate_response('ExecutionStartPatch', 200, Body, ValidatorState) -> - validate_response_body('', '', Body, ValidatorState); -validate_response('ExecutionStartPatch', 409, Body, ValidatorState) -> - validate_response_body('', '', Body, ValidatorState); -validate_response('ExecutionStartPatch', 500, Body, ValidatorState) -> - validate_response_body('Error', 'Error', Body, ValidatorState); - -validate_response('ExecutionStopPatch', 200, Body, ValidatorState) -> - validate_response_body('', '', Body, ValidatorState); -validate_response('ExecutionStopPatch', 409, Body, ValidatorState) -> - validate_response_body('', '', Body, ValidatorState); -validate_response('ExecutionStopPatch', 500, Body, ValidatorState) -> - validate_response_body('Error', 'Error', Body, ValidatorState); - -validate_response('ExecutionUpdateSettingsPatch', 200, Body, ValidatorState) -> - validate_response_body('', '', Body, ValidatorState); -validate_response('ExecutionUpdateSettingsPatch', 409, Body, ValidatorState) -> - validate_response_body('', '', Body, ValidatorState); -validate_response('ExecutionUpdateSettingsPatch', 500, Body, ValidatorState) -> - validate_response_body('Error', 'Error', Body, ValidatorState); - - -validate_response('ScenariosDefaultsIdGet', 200, Body, ValidatorState) -> - validate_response_body('ScenarioDefaults', 'ScenarioDefaults', Body, ValidatorState); -validate_response('ScenariosDefaultsIdGet', 404, Body, ValidatorState) -> - validate_response_body('', '', Body, ValidatorState); - -validate_response('ScenariosGet', 200, Body, ValidatorState) -> - validate_response_body('ScenarioList', 'ScenarioList', Body, ValidatorState); - -validate_response('ScenariosInfoIdGet', 200, Body, ValidatorState) -> - validate_response_body('ScenarioInfo', 'ScenarioInfo', Body, ValidatorState); -validate_response('ScenariosInfoIdGet', 404, Body, ValidatorState) -> - validate_response_body('', '', Body, ValidatorState); - -validate_response('ScenariosUploadPut', 200, Body, ValidatorState) -> - validate_response_body('UploadResp', 'UploadResp', Body, ValidatorState); -validate_response('ScenariosUploadPut', 400, Body, ValidatorState) -> - validate_response_body('Error', 'Error', Body, ValidatorState); - - -validate_response('NodesGet', 200, Body, ValidatorState) -> - validate_response_body('NodesKV', 'NodesKV', Body, ValidatorState); - -validate_response('StatusGet', 200, Body, ValidatorState) -> - validate_response_body('AmocStatus', 'AmocStatus', Body, ValidatorState); - -validate_response('StatusNodeGet', 200, Body, ValidatorState) -> - validate_response_body('AmocStatus', 'AmocStatus', Body, ValidatorState); -validate_response('StatusNodeGet', 404, Body, ValidatorState) -> - validate_response_body('', '', Body, ValidatorState); - - -validate_response(_OperationID, _Code, _Body, _ValidatorState) -> - ok. - -validate_response_body('list', ReturnBaseType, Body, ValidatorState) -> +validate_response_body(list, ReturnBaseType, Body, ValidatorState) -> [ - validate(schema, ReturnBaseType, Item, ValidatorState) + validate(schema, Item, ReturnBaseType, ValidatorState) || Item <- Body]; validate_response_body(_, ReturnBaseType, Body, ValidatorState) -> - validate(schema, ReturnBaseType, Body, ValidatorState). + validate(schema, Body, ReturnBaseType, ValidatorState). -%%% -validate(Rule = required, Name, Value, _ValidatorState) -> - case Value of - undefined -> validation_error(Rule, Name); - _ -> ok - end; - -validate(not_required, _Name, _Value, _ValidatorState) -> +-spec validate(rule(), term(), request_param(), jesse_state:state()) -> + ok | {ok, term()}. +validate(required, undefined, ReqParamName, _) -> + validation_error(required, ReqParamName, undefined); +validate(required, _Value, _, _) -> ok; - -validate(_, _Name, undefined, _ValidatorState) -> +validate(not_required, _Value, _, _) -> ok; - -validate(Rule = {type, 'integer'}, Name, Value, _ValidatorState) -> - try - {ok, amoc_rest_utils:to_int(Value)} - catch - error:badarg -> - validation_error(Rule, Name) +validate(_, undefined, _, _) -> + ok; +validate({type, boolean}, Value, _, _) when is_boolean(Value) -> + ok; +validate({type, integer}, Value, _, _) when is_integer(Value) -> + ok; +validate({type, float}, Value, _, _) when is_float(Value) -> + ok; +validate({type, binary}, Value, _, _) when is_binary(Value) -> + ok; +validate({max, Max}, Value, _, _) when Value =< Max -> + ok; +validate({min, Min}, Value, _, _) when Min =< Value -> + ok; +validate({exclusive_max, Max}, Value, _, _) when Value < Max -> + ok; +validate({exclusive_min, Min}, Value, _, _) when Min < Value -> + ok; +validate({max_length, MaxLength}, Value, _, _) when is_binary(Value), byte_size(Value) =< MaxLength -> + ok; +validate({min_length, MinLength}, Value, _, _) when is_binary(Value), MinLength =< byte_size(Value) -> + ok; +validate(Rule = {type, byte}, Value, ReqParamName, _) when is_binary(Value) -> + try base64:decode(Value) of + Decoded -> {ok, Decoded} + catch error:_Error -> validation_error(Rule, ReqParamName, Value) end; - -validate(Rule = {type, 'float'}, Name, Value, _ValidatorState) -> +validate(Rule = {type, boolean}, Value, ReqParamName, _) when is_binary(Value) -> + case to_binary(string:lowercase(Value)) of + <<"true">> -> {ok, true}; + <<"false">> -> {ok, false}; + _ -> validation_error(Rule, ReqParamName, Value) + end; +validate(Rule = {type, integer}, Value, ReqParamName, _) when is_binary(Value) -> try - {ok, amoc_rest_utils:to_float(Value)} + {ok, binary_to_integer(Value)} catch error:badarg -> - validation_error(Rule, Name) - end; - -validate(Rule = {type, 'binary'}, Name, Value, _ValidatorState) -> - case is_binary(Value) of - true -> ok; - false -> validation_error(Rule, Name) + validation_error(Rule, ReqParamName, Value) end; - -validate(_Rule = {type, 'boolean'}, _Name, Value, _ValidatorState) when is_boolean(Value) -> - {ok, Value}; - -validate(Rule = {type, 'boolean'}, Name, Value, _ValidatorState) -> - V = binary_to_lower(Value), +validate(Rule = {type, float}, Value, ReqParamName, _) when is_binary(Value) -> try - case binary_to_existing_atom(V, utf8) of - B when is_boolean(B) -> {ok, B}; - _ -> validation_error(Rule, Name) - end + {ok, binary_to_float(Value)} catch error:badarg -> - validation_error(Rule, Name) + validation_error(Rule, ReqParamName, Value) end; - -validate(Rule = {type, 'date'}, Name, Value, _ValidatorState) -> +validate(Rule = {type, date}, Value, ReqParamName, _) -> case is_binary(Value) of true -> ok; - false -> validation_error(Rule, Name) + false -> validation_error(Rule, ReqParamName, Value) end; - -validate(Rule = {type, 'datetime'}, Name, Value, _ValidatorState) -> - case is_binary(Value) of - true -> ok; - false -> validation_error(Rule, Name) +validate(Rule = {type, datetime}, Value, ReqParamName, _) -> + try calendar:rfc3339_to_system_time(binary_to_list(Value)) of + _ -> ok + catch error:_Error -> validation_error(Rule, ReqParamName, Value) end; - -validate(Rule = {enum, Values}, Name, Value, _ValidatorState) -> +validate(Rule = {enum, Values}, Value, ReqParamName, _) -> try FormattedValue = erlang:binary_to_existing_atom(Value, utf8), case lists:member(FormattedValue, Values) of true -> {ok, FormattedValue}; - false -> validation_error(Rule, Name) + false -> validation_error(Rule, ReqParamName, Value) end catch error:badarg -> - validation_error(Rule, Name) - end; - -validate(Rule = {max, Max}, Name, Value, _ValidatorState) -> - case Value =< Max of - true -> ok; - false -> validation_error(Rule, Name) - end; - -validate(Rule = {exclusive_max, ExclusiveMax}, Name, Value, _ValidatorState) -> - case Value > ExclusiveMax of - true -> ok; - false -> validation_error(Rule, Name) - end; - -validate(Rule = {min, Min}, Name, Value, _ValidatorState) -> - case Value >= Min of - true -> ok; - false -> validation_error(Rule, Name) - end; - -validate(Rule = {exclusive_min, ExclusiveMin}, Name, Value, _ValidatorState) -> - case Value =< ExclusiveMin of - true -> ok; - false -> validation_error(Rule, Name) - end; - -validate(Rule = {max_length, MaxLength}, Name, Value, _ValidatorState) -> - case size(Value) =< MaxLength of - true -> ok; - false -> validation_error(Rule, Name) + validation_error(Rule, ReqParamName, Value) end; - -validate(Rule = {min_length, MinLength}, Name, Value, _ValidatorState) -> - case size(Value) >= MinLength of - true -> ok; - false -> validation_error(Rule, Name) - end; - -validate(Rule = {pattern, Pattern}, Name, Value, _ValidatorState) -> +validate(Rule = {pattern, Pattern}, Value, ReqParamName, _) -> {ok, MP} = re:compile(Pattern), case re:run(Value, MP) of {match, _} -> ok; - _ -> validation_error(Rule, Name) + _ -> validation_error(Rule, ReqParamName, Value) end; - -validate(Rule = schema, Name, Value, ValidatorState) -> - Definition = list_to_binary("#/components/schemas/" ++ amoc_rest_utils:to_list(Name)), +validate(schema, Value, ReqParamName, ValidatorState) -> + Definition = iolist_to_binary(["#/components/schemas/", atom_to_binary(ReqParamName, utf8)]), + validate({schema, object, Definition}, Value, ReqParamName, ValidatorState); +validate({schema, list, Definition}, Value, ReqParamName, ValidatorState) -> + lists:foreach( + fun(Item) -> + validate({schema, object, Definition}, Item, ReqParamName, ValidatorState) + end, Value); +validate(Rule = {schema, object, Definition}, Value, ReqParamName, ValidatorState) -> try _ = validate_with_schema(Value, Definition, ValidatorState), ok @@ -421,7 +447,7 @@ validate(Rule = schema, Name, Value, ValidatorState) -> type => schema_invalid, error => Error }, - validation_error(Rule, Name, Info); + validation_error(Rule, ReqParamName, Value, Info); throw:[{data_invalid, Schema, Error, _, Path} | _] -> Info = #{ type => data_invalid, @@ -429,59 +455,63 @@ validate(Rule = schema, Name, Value, ValidatorState) -> schema => Schema, path => Path }, - validation_error(Rule, Name, Info) + validation_error(Rule, ReqParamName, Value, Info) end; +validate(Rule, Value, ReqParamName, _) -> + validation_error(Rule, ReqParamName, Value). -validate(Rule, Name, _Value, _ValidatorState) -> - error_logger:info_msg("Can't validate ~p with ~p", [Name, Rule]), - error({unknown_validation_rule, Rule}). - --spec validation_error(Rule :: any(), Name :: any()) -> no_return(). - -validation_error(ViolatedRule, Name) -> - validation_error(ViolatedRule, Name, #{}). - --spec validation_error(Rule :: any(), Name :: any(), Info :: #{}) -> no_return(). +-spec validation_error(rule(), request_param(), term()) -> no_return(). +validation_error(ViolatedRule, Name, Value) -> + validation_error(ViolatedRule, Name, Value, #{}). -validation_error(ViolatedRule, Name, Info) -> - throw({wrong_param, Name, ViolatedRule, Info}). +-spec validation_error(rule(), request_param(), term(), Info :: #{_ := _}) -> no_return(). +validation_error(ViolatedRule, Name, Value, Info) -> + throw({wrong_param, Name, Value, ViolatedRule, Info}). --spec get_value(body | qs_val | header | binding, Name :: any(), Req0 :: cowboy_req:req()) -> - {Value :: any(), Req :: cowboy_req:req()} | - {error, Reason :: any(), Req :: cowboy_req:req()}. +-spec get_value(body | qs_val | header | binding, request_param(), cowboy_req:req()) -> + {any(), cowboy_req:req()} | + {error, any(), cowboy_req:req()}. get_value(body, _Name, Req0) -> - {ok, Body, Req} = cowboy_req:read_body(Req0), + {ok, Body, Req} = read_entire_body(Req0), case prepare_body(Body) of {error, Reason} -> {error, Reason, Req}; Value -> {Value, Req} end; - get_value(qs_val, Name, Req) -> QS = cowboy_req:parse_qs(Req), - Value = amoc_rest_utils:get_opt(amoc_rest_utils:to_qs(Name), QS), + Value = get_opt(to_qs(Name), QS), {Value, Req}; - get_value(header, Name, Req) -> Headers = cowboy_req:headers(Req), - Value = maps:get(amoc_rest_utils:to_header(Name), Headers, undefined), + Value = maps:get(to_header(Name), Headers, undefined), {Value, Req}; - get_value(binding, Name, Req) -> - Value = cowboy_req:binding(amoc_rest_utils:to_binding(Name), Req), + Value = cowboy_req:binding(Name, Req), {Value, Req}. +-spec read_entire_body(cowboy_req:req()) -> {ok, binary(), cowboy_req:req()}. +read_entire_body(Req) -> + read_entire_body(Req, []). + +-spec read_entire_body(cowboy_req:req(), iodata()) -> {ok, binary(), cowboy_req:req()}. +read_entire_body(Request, Acc) -> % { + case cowboy_req:read_body(Request) of + {ok, Data, NewRequest} -> + {ok, iolist_to_binary(lists:reverse([Data | Acc])), NewRequest}; + {more, Data, NewRequest} -> + read_entire_body(NewRequest, [Data | Acc]) + end. + +prepare_body(<<>>) -> + <<>>; prepare_body(Body) -> - case Body of - <<"">> -> <<"">>; - _ -> - try - jsx:decode(Body, [return_maps]) - catch - error:_ -> - {error, {invalid_body, not_json, Body}} - end + try + json:decode(Body) + catch + error:Error -> + {error, {invalid_json, Body, Error}} end. validate_with_schema(Body, Definition, ValidatorState) -> @@ -491,23 +521,71 @@ validate_with_schema(Body, Definition, ValidatorState) -> ValidatorState ). -prepare_param(Rules, Name, Value, ValidatorState) -> +-spec prepare_param([rule()], request_param(), term(), jesse_state:state()) -> + {ok, term()} | {error, Reason :: any()}. +prepare_param(Rules, ReqParamName, Value, ValidatorState) -> + Fun = fun(Rule, Acc) -> + case validate(Rule, Acc, ReqParamName, ValidatorState) of + ok -> Acc; + {ok, Prepared} -> Prepared + end + end, try - Result = lists:foldl( - fun(Rule, Acc) -> - case validate(Rule, Name, Acc, ValidatorState) of - ok -> Acc; - {ok, Prepared} -> Prepared - end - end, - Value, - Rules - ), + Result = lists:foldl(Fun, Value, Rules), {ok, Result} catch throw:Reason -> {error, Reason} end. -binary_to_lower(V) when is_binary(V) -> - list_to_binary(string:to_lower(amoc_rest_utils:to_list(V))). +-spec to_binary(iodata()) -> binary(). +to_binary(V) when is_binary(V) -> V; +to_binary(V) when is_list(V) -> iolist_to_binary(V). + +-spec to_header(request_param()) -> binary(). +to_header(Name) -> + to_binary(string:lowercase(atom_to_binary(Name, utf8))). + +-spec to_qs(request_param()) -> binary(). +to_qs(Name) -> + atom_to_binary(Name, utf8). + +-spec get_opt(any(), []) -> any(). +get_opt(Key, Opts) -> + get_opt(Key, Opts, undefined). + +-spec get_opt(any(), [], any()) -> any(). +get_opt(Key, Opts, Default) -> + case lists:keyfind(Key, 1, Opts) of + {_, Value} -> Value; + false -> Default + end. + +get_openapi_path() -> + {ok, AppName} = application:get_application(?MODULE), + filename:join(priv_dir(AppName), "openapi.json"). + +-include_lib("kernel/include/file.hrl"). + +-spec priv_dir(Application :: atom()) -> file:name_all(). +priv_dir(AppName) -> + case code:priv_dir(AppName) of + Value when is_list(Value) -> + Value ++ "/"; + _Error -> + select_priv_dir([filename:join(["apps", atom_to_list(AppName), "priv"]), "priv"]) + end. + +select_priv_dir(Paths) -> + case lists:dropwhile(fun test_priv_dir/1, Paths) of + [Path | _] -> Path; + _ -> exit(no_priv_dir) + end. + +test_priv_dir(Path) -> + case file:read_file_info(Path) of + {ok, #file_info{type = directory}} -> + false; + _ -> + true + end. diff --git a/src/amoc_rest_auth.erl b/src/amoc_rest_auth.erl new file mode 100644 index 0000000..9241621 --- /dev/null +++ b/src/amoc_rest_auth.erl @@ -0,0 +1,45 @@ +-module(amoc_rest_auth). + +-export([authorize_api_key/5]). + +-spec authorize_api_key(amoc_rest_logic_handler:api_key_callback(), + amoc_rest_api:operation_id(), + header | qs_val, + iodata() | atom(), + cowboy_req:req()) -> + {true, amoc_rest_logic_handler:context(), cowboy_req:req()} | + {false, binary(), cowboy_req:req()}. +authorize_api_key(Handler, OperationID, From, KeyParam, Req0) -> + {ApiKey, Req} = get_api_key(From, KeyParam, Req0), + case ApiKey of + undefined -> + AuthHeader = <<>>, + {false, AuthHeader, Req}; + _ -> + case Handler(OperationID, ApiKey) of + {true, Context} -> + {true, Context, Req}; + {false, AuthHeader} -> + {false, AuthHeader, Req} + end + end. + +get_api_key(header, KeyParam, Req) -> + Headers = cowboy_req:headers(Req), + {maps:get(KeyParam, Headers, undefined), Req}; +get_api_key(qs_val, KeyParam, Req) -> + QS = cowboy_req:parse_qs(Req), + {get_opt(KeyParam, QS), Req}. + +-spec get_opt(any(), []) -> any(). +get_opt(Key, Opts) -> + get_opt(Key, Opts, undefined). + +-spec get_opt(any(), [], any()) -> any(). +get_opt(Key, Opts, Default) -> + case lists:keyfind(Key, 1, Opts) of + {_, Value} -> + Value; + false -> + Default + end. diff --git a/src/amoc_rest_default_logic_handler.erl b/src/amoc_rest_default_logic_handler.erl deleted file mode 100644 index 4bbf1b0..0000000 --- a/src/amoc_rest_default_logic_handler.erl +++ /dev/null @@ -1,20 +0,0 @@ --module(amoc_rest_default_logic_handler). - --behaviour(amoc_rest_logic_handler). - --export([handle_request/3]). - - --spec handle_request( - OperationID :: amoc_rest_api:operation_id(), - Req :: cowboy_req:req(), - Context :: #{} -) -> - {Status :: cowboy:http_status(), Headers :: cowboy:http_headers(), Body :: jsx:json_term()}. - -handle_request(OperationID, Req, Context) -> - error_logger:error_msg( - "Got not implemented request to process: ~p~n", - [{OperationID, Req, Context}] - ), - {501, #{}, #{}}. diff --git a/src/amoc_rest_execution_handler.erl b/src/amoc_rest_execution_handler.erl index 0e7f097..82dfbbe 100644 --- a/src/amoc_rest_execution_handler.erl +++ b/src/amoc_rest_execution_handler.erl @@ -1,269 +1,183 @@ -%% basic handler -module(amoc_rest_execution_handler). +-moduledoc """ +Exposes the following operation IDs: + +- `PATCH` to `/execution/add_users`, OperationId: `addUsers`: +add new users. + + +- `PATCH` to `/execution/remove_users`, OperationId: `removeUsers`: +stop users. + + +- `PATCH` to `/execution/start`, OperationId: `startScenario`: +Start scenario. + + +- `PATCH` to `/execution/stop`, OperationId: `stopScenario`: +Stop scenario. + + +- `PATCH` to `/execution/update_settings`, OperationId: `updateSettings`: +update scenario settings. + + +""". + +-behaviour(cowboy_rest). + +-include_lib("kernel/include/logger.hrl"). %% Cowboy REST callbacks --export([allowed_methods/2]). -export([init/2]). --export([allow_missing_post/2]). +-export([allowed_methods/2]). -export([content_types_accepted/2]). -export([content_types_provided/2]). -export([delete_resource/2]). -export([is_authorized/2]). --export([known_content_type/2]). --export([malformed_request/2]). -export([valid_content_headers/2]). --export([valid_entity_length/2]). - -%% Handlers --export([handle_request_json/2]). +-export([handle_type_accepted/2, handle_type_provided/2]). --record(state, { - operation_id :: amoc_rest_api:operation_id(), - logic_handler :: atom(), - validator_state :: jesse_state:state(), - context=#{} :: #{} -}). +-ignore_xref([handle_type_accepted/2, handle_type_provided/2]). --type state() :: state(). +-export_type([class/0, operation_id/0]). --spec init(Req :: cowboy_req:req(), Opts :: amoc_rest_router:init_opts()) -> - {cowboy_rest, Req :: cowboy_req:req(), State :: state()}. +-type class() :: 'execution'. -init(Req, {Operations, LogicHandler, ValidatorState}) -> - Method = cowboy_req:method(Req), - OperationID = maps:get(Method, Operations, undefined), +-type operation_id() :: + 'addUsers' %% add new users + | 'removeUsers' %% stop users + | 'startScenario' %% Start scenario + | 'stopScenario' %% Stop scenario + | 'updateSettings'. %% update scenario settings - error_logger:info_msg("Attempt to process operation: ~p", [OperationID]), - State = #state{ - operation_id = OperationID, - logic_handler = LogicHandler, - validator_state = ValidatorState - }, - {cowboy_rest, Req, State}. +-record(state, + {operation_id :: operation_id(), + accept_callback :: amoc_rest_logic_handler:accept_callback(), + provide_callback :: amoc_rest_logic_handler:provide_callback(), + api_key_handler :: amoc_rest_logic_handler:api_key_callback(), + context = #{} :: amoc_rest_logic_handler:context()}). --spec allowed_methods(Req :: cowboy_req:req(), State :: state()) -> - {Value :: [binary()], Req :: cowboy_req:req(), State :: state()}. +-type state() :: #state{}. +-spec init(cowboy_req:req(), amoc_rest_router:init_opts()) -> + {cowboy_rest, cowboy_req:req(), state()}. +init(Req, {Operations, Module}) -> + Method = cowboy_req:method(Req), + OperationID = maps:get(Method, Operations, undefined), + ?LOG_INFO(#{what => "Attempt to process operation", + method => Method, + operation_id => OperationID}), + State = #state{operation_id = OperationID, + accept_callback = fun Module:accept_callback/4, + provide_callback = fun Module:provide_callback/4, + api_key_handler = fun Module:authorize_api_key/2}, + {cowboy_rest, Req, State}. -allowed_methods( - Req, - State = #state{ - operation_id = 'ExecutionAddUsersPatch' - } -) -> +-spec allowed_methods(cowboy_req:req(), state()) -> + {[binary()], cowboy_req:req(), state()}. +allowed_methods(Req, #state{operation_id = 'addUsers'} = State) -> {[<<"PATCH">>], Req, State}; - -allowed_methods( - Req, - State = #state{ - operation_id = 'ExecutionRemoveUsersPatch' - } -) -> +allowed_methods(Req, #state{operation_id = 'removeUsers'} = State) -> {[<<"PATCH">>], Req, State}; - -allowed_methods( - Req, - State = #state{ - operation_id = 'ExecutionStartPatch' - } -) -> +allowed_methods(Req, #state{operation_id = 'startScenario'} = State) -> {[<<"PATCH">>], Req, State}; - -allowed_methods( - Req, - State = #state{ - operation_id = 'ExecutionStopPatch' - } -) -> +allowed_methods(Req, #state{operation_id = 'stopScenario'} = State) -> {[<<"PATCH">>], Req, State}; - -allowed_methods( - Req, - State = #state{ - operation_id = 'ExecutionUpdateSettingsPatch' - } -) -> +allowed_methods(Req, #state{operation_id = 'updateSettings'} = State) -> {[<<"PATCH">>], Req, State}; - allowed_methods(Req, State) -> {[], Req, State}. --spec is_authorized(Req :: cowboy_req:req(), State :: state()) -> - { - Value :: true | {false, AuthHeader :: iodata()}, - Req :: cowboy_req:req(), - State :: state() - }. +-spec is_authorized(cowboy_req:req(), state()) -> + {true | {false, iodata()}, cowboy_req:req(), state()}. is_authorized(Req, State) -> {true, Req, State}. --spec content_types_accepted(Req :: cowboy_req:req(), State :: state()) -> - { - Value :: [{binary(), AcceptResource :: atom()}], - Req :: cowboy_req:req(), - State :: state() - }. - -content_types_accepted(Req, State) -> +-spec content_types_accepted(cowboy_req:req(), state()) -> + {[{binary(), atom()}], cowboy_req:req(), state()}. +content_types_accepted(Req, #state{operation_id = 'addUsers'} = State) -> {[ - {undefined, handle_request_json}, - {<<"application/json">>, handle_request_json} - ], Req, State}. - --spec valid_content_headers(Req :: cowboy_req:req(), State :: state()) -> - {Value :: boolean(), Req :: cowboy_req:req(), State :: state()}. - -valid_content_headers( - Req0, - State = #state{ - operation_id = 'ExecutionAddUsersPatch' - } -) -> - Headers = [], - {Result, Req} = validate_headers(Headers, Req0), - {Result, Req, State}; - -valid_content_headers( - Req0, - State = #state{ - operation_id = 'ExecutionRemoveUsersPatch' - } -) -> - Headers = [], - {Result, Req} = validate_headers(Headers, Req0), - {Result, Req, State}; - -valid_content_headers( - Req0, - State = #state{ - operation_id = 'ExecutionStartPatch' - } -) -> - Headers = [], - {Result, Req} = validate_headers(Headers, Req0), - {Result, Req, State}; - -valid_content_headers( - Req0, - State = #state{ - operation_id = 'ExecutionStopPatch' - } -) -> - Headers = [], - {Result, Req} = validate_headers(Headers, Req0), - {Result, Req, State}; - -valid_content_headers( - Req0, - State = #state{ - operation_id = 'ExecutionUpdateSettingsPatch' - } -) -> - Headers = [], - {Result, Req} = validate_headers(Headers, Req0), - {Result, Req, State}; + {<<"application/json">>, handle_type_accepted} + ], Req, State}; +content_types_accepted(Req, #state{operation_id = 'removeUsers'} = State) -> + {[ + {<<"application/json">>, handle_type_accepted} + ], Req, State}; +content_types_accepted(Req, #state{operation_id = 'startScenario'} = State) -> + {[ + {<<"application/json">>, handle_type_accepted} + ], Req, State}; +content_types_accepted(Req, #state{operation_id = 'stopScenario'} = State) -> + {[ + {<<"application/json">>, handle_type_accepted} + ], Req, State}; +content_types_accepted(Req, #state{operation_id = 'updateSettings'} = State) -> + {[ + {<<"application/json">>, handle_type_accepted} + ], Req, State}; +content_types_accepted(Req, State) -> + {[], Req, State}. +-spec valid_content_headers(cowboy_req:req(), state()) -> + {boolean(), cowboy_req:req(), state()}. +valid_content_headers(Req, #state{operation_id = 'addUsers'} = State) -> + {true, Req, State}; +valid_content_headers(Req, #state{operation_id = 'removeUsers'} = State) -> + {true, Req, State}; +valid_content_headers(Req, #state{operation_id = 'startScenario'} = State) -> + {true, Req, State}; +valid_content_headers(Req, #state{operation_id = 'stopScenario'} = State) -> + {true, Req, State}; +valid_content_headers(Req, #state{operation_id = 'updateSettings'} = State) -> + {true, Req, State}; valid_content_headers(Req, State) -> {false, Req, State}. --spec content_types_provided(Req :: cowboy_req:req(), State :: state()) -> - { - Value :: [{binary(), ProvideResource :: atom()}], - Req :: cowboy_req:req(), - State :: state() - }. - -content_types_provided(Req, State) -> +-spec content_types_provided(cowboy_req:req(), state()) -> + {[{binary(), atom()}], cowboy_req:req(), state()}. +content_types_provided(Req, #state{operation_id = 'addUsers'} = State) -> {[ - {<<"application/json">>, handle_request_json} - ], Req, State}. - --spec malformed_request(Req :: cowboy_req:req(), State :: state()) -> - {Value :: false, Req :: cowboy_req:req(), State :: state()}. - -malformed_request(Req, State) -> - {false, Req, State}. - --spec allow_missing_post(Req :: cowboy_req:req(), State :: state()) -> - {Value :: false, Req :: cowboy_req:req(), State :: state()}. - -allow_missing_post(Req, State) -> - {false, Req, State}. - --spec delete_resource(Req :: cowboy_req:req(), State :: state()) -> - processed_response(). + {<<"application/json">>, handle_type_provided} + ], Req, State}; +content_types_provided(Req, #state{operation_id = 'removeUsers'} = State) -> + {[ + {<<"application/json">>, handle_type_provided} + ], Req, State}; +content_types_provided(Req, #state{operation_id = 'startScenario'} = State) -> + {[ + {<<"application/json">>, handle_type_provided} + ], Req, State}; +content_types_provided(Req, #state{operation_id = 'stopScenario'} = State) -> + {[ + {<<"application/json">>, handle_type_provided} + ], Req, State}; +content_types_provided(Req, #state{operation_id = 'updateSettings'} = State) -> + {[ + {<<"application/json">>, handle_type_provided} + ], Req, State}; +content_types_provided(Req, State) -> + {[], Req, State}. +-spec delete_resource(cowboy_req:req(), state()) -> + {boolean(), cowboy_req:req(), state()}. delete_resource(Req, State) -> - handle_request_json(Req, State). - --spec known_content_type(Req :: cowboy_req:req(), State :: state()) -> - {Value :: true, Req :: cowboy_req:req(), State :: state()}. - -known_content_type(Req, State) -> - {true, Req, State}. - --spec valid_entity_length(Req :: cowboy_req:req(), State :: state()) -> - {Value :: true, Req :: cowboy_req:req(), State :: state()}. - -valid_entity_length(Req, State) -> - %% @TODO check the length - {true, Req, State}. - -%%%% --type result_ok() :: { - ok, - {Status :: cowboy:http_status(), Headers :: cowboy:http_headers(), Body :: iodata()} -}. - --type result_error() :: {error, Reason :: any()}. - --type processed_response() :: {stop, cowboy_req:req(), state()}. - --spec process_response(result_ok() | result_error(), cowboy_req:req(), state()) -> - processed_response(). - -process_response(Response, Req0, State = #state{operation_id = OperationID}) -> - case Response of - {ok, {Code, Headers, Body}} -> - Req = cowboy_req:reply(Code, Headers, Body, Req0), - {stop, Req, State}; - {error, Message} -> - error_logger:error_msg("Unable to process request for ~p: ~p", [OperationID, Message]), - - Req = cowboy_req:reply(400, Req0), - {stop, Req, State} - end. - --spec handle_request_json(cowboy_req:req(), state()) -> processed_response(). - -handle_request_json( - Req0, - State = #state{ - operation_id = OperationID, - logic_handler = LogicHandler, - validator_state = ValidatorState - } -) -> - case amoc_rest_api:populate_request(OperationID, Req0, ValidatorState) of - {ok, Populated, Req1} -> - {Code, Headers, Body} = amoc_rest_logic_handler:handle_request( - LogicHandler, - OperationID, - Req1, - maps:merge(State#state.context, Populated) - ), - _ = amoc_rest_api:validate_response( - OperationID, - Code, - Body, - ValidatorState - ), - PreparedBody = jsx:encode(Body), - Response = {ok, {Code, Headers, PreparedBody}}, - process_response(Response, Req1, State); - {error, Reason, Req1} -> - process_response({error, Reason}, Req1, State) - end. - -validate_headers(_, Req) -> {true, Req}. + {Res, Req1, State1} = handle_type_accepted(Req, State), + {true =:= Res, Req1, State1}. + +-spec handle_type_accepted(cowboy_req:req(), state()) -> + { amoc_rest_logic_handler:accept_callback_return(), cowboy_req:req(), state()}. +handle_type_accepted(Req, #state{operation_id = OperationID, + accept_callback = Handler, + context = Context} = State) -> + {Res, Req1, Context1} = Handler(execution, OperationID, Req, Context), + {Res, Req1, State#state{context = Context1}}. + +-spec handle_type_provided(cowboy_req:req(), state()) -> + { amoc_rest_logic_handler:provide_callback_return(), cowboy_req:req(), state()}. +handle_type_provided(Req, #state{operation_id = OperationID, + provide_callback = Handler, + context = Context} = State) -> + {Res, Req1, Context1} = Handler(execution, OperationID, Req, Context), + {Res, Req1, State#state{context = Context1}}. diff --git a/src/amoc_rest_logic_handler.erl b/src/amoc_rest_logic_handler.erl index 76c2098..4db0e2b 100644 --- a/src/amoc_rest_logic_handler.erl +++ b/src/amoc_rest_logic_handler.erl @@ -1,33 +1,67 @@ -module(amoc_rest_logic_handler). --export([handle_request/4]). --export([authorize_api_key/3]). --type context() :: #{binary() => any()}. --type handler_response() ::{ - Status :: cowboy:http_status(), - Headers :: cowboy:http_headers(), - Body :: jsx:json_term() -}. +-include_lib("kernel/include/logger.hrl"). --export_type([handler_response/0]). +-type accept_callback_return() :: + stop + | boolean() + | {true, iodata()} + | {created, iodata()} + | {see_other, iodata()}. +-type provide_callback_return() :: + stop + | cowboy_req:resp_body(). +-type api_key_callback() :: + fun((amoc_rest_api:operation_id(), binary()) -> {true, context()} | {false, iodata()}). +-type accept_callback() :: + fun((amoc_rest_api:class(), amoc_rest_api:operation_id(), cowboy_req:req(), context()) -> + {accept_callback_return(), cowboy_req:req(), context()}). +-type provide_callback() :: + fun((amoc_rest_api:class(), amoc_rest_api:operation_id(), cowboy_req:req(), context()) -> + {cowboy_req:resp_body(), cowboy_req:req(), context()}). +-type context() :: #{_ := _}. +-export_type([context/0, api_key_callback/0, + accept_callback_return/0, provide_callback_return/0, + accept_callback/0, provide_callback/0]). +-optional_callbacks([api_key_callback/2]). --callback handle_request(OperationID :: amoc_rest_api:operation_id(), cowboy_req:req(), Context :: context()) -> - handler_response(). +-callback api_key_callback(amoc_rest_api:operation_id(), binary()) -> + {true, context()} | {false, iodata()}. --spec handle_request( - Handler :: atom(), - OperationID :: amoc_rest_api:operation_id(), - Request :: cowboy_req:req(), - Context :: context() -) -> - handler_response(). +-callback accept_callback(amoc_rest_api:class(), amoc_rest_api:operation_id(), cowboy_req:req(), context()) -> + {accept_callback_return(), cowboy_req:req(), context()}. -handle_request(Handler, OperationID, Req, Context) -> - Handler:handle_request(OperationID, Req, Context). +-callback provide_callback(amoc_rest_api:class(), amoc_rest_api:operation_id(), cowboy_req:req(), context()) -> + {cowboy_req:resp_body(), cowboy_req:req(), context()}. --spec authorize_api_key(Handler :: atom(), OperationID :: amoc_rest_api:operation_id(), ApiKey :: binary()) -> - Result :: false. -authorize_api_key(_Handler, _OperationID, _ApiKey) -> - false. +-export([api_key_callback/2, accept_callback/4, provide_callback/4]). +-ignore_xref([api_key_callback/2, accept_callback/4, provide_callback/4]). + +-spec api_key_callback(amoc_rest_api:operation_id(), binary()) -> {true, #{}}. +api_key_callback(OperationID, ApiKey) -> + ?LOG_ERROR(#{what => "Got not implemented api_key_callback request", + operation_id => OperationID, + api_key => ApiKey}), + {true, #{}}. + +-spec accept_callback(amoc_rest_api:class(), amoc_rest_api:operation_id(), cowboy_req:req(), context()) -> + {accept_callback_return(), cowboy_req:req(), context()}. +accept_callback(Class, OperationID, Req, Context) -> + ?LOG_ERROR(#{what => "Got not implemented request to process", + class => Class, + operation_id => OperationID, + request => Req, + context => Context}), + {false, Req, Context}. + +-spec provide_callback(amoc_rest_api:class(), amoc_rest_api:operation_id(), cowboy_req:req(), context()) -> + {cowboy_req:resp_body(), cowboy_req:req(), context()}. +provide_callback(Class, OperationID, Req, Context) -> + ?LOG_ERROR(#{what => "Got not implemented request to process", + class => Class, + operation_id => OperationID, + request => Req, + context => Context}), + {<<>>, Req, Context}. diff --git a/src/amoc_rest_router.erl b/src/amoc_rest_router.erl index 259a22b..b534084 100644 --- a/src/amoc_rest_router.erl +++ b/src/amoc_rest_router.erl @@ -2,128 +2,133 @@ -export([get_paths/1]). --type operations() :: #{ - Method :: binary() => amoc_rest_api:operation_id() -}. - --type init_opts() :: { - Operations :: operations(), - LogicHandler :: atom(), - ValidatorState :: jesse_state:state() -}. +-type method() :: binary(). +-type operations() :: #{method() => amoc_rest_api:operation_id()}. +-type init_opts() :: {operations(), module()}. -export_type([init_opts/0]). --spec get_paths(LogicHandler :: atom()) -> [{'_',[{ - Path :: string(), - Handler :: atom(), - InitOpts :: init_opts() -}]}]. - +-spec get_paths(LogicHandler :: module()) -> cowboy_router:routes(). get_paths(LogicHandler) -> - ValidatorState = prepare_validator(), PreparedPaths = maps:fold( - fun(Path, #{operations := Operations, handler := Handler}, Acc) -> - [{Path, Handler, Operations} | Acc] - end, - [], - group_paths() - ), - [ - {'_', - [{P, H, {O, LogicHandler, ValidatorState}} || {P, H, O} <- PreparedPaths] - } - ]. + fun(Path, #{operations := Operations, handler := Handler}, Acc) -> + [{Path, Handler, Operations} | Acc] + end, [], group_paths() + ), + [{'_', [{P, H, {O, LogicHandler}} || {P, H, O} <- PreparedPaths]}]. group_paths() -> maps:fold( - fun(OperationID, #{path := Path, method := Method, handler := Handler}, Acc) -> - case maps:find(Path, Acc) of - {ok, PathInfo0 = #{operations := Operations0}} -> - Operations = Operations0#{Method => OperationID}, - PathInfo = PathInfo0#{operations => Operations}, - Acc#{Path => PathInfo}; - error -> - Operations = #{Method => OperationID}, - PathInfo = #{handler => Handler, operations => Operations}, - Acc#{Path => PathInfo} - end - end, - #{}, - get_operations() - ). + fun(OperationID, #{servers := Servers, base_path := BasePath, path := Path, + method := Method, handler := Handler}, Acc) -> + FullPaths = build_full_paths(Servers, BasePath, Path), + merge_paths(FullPaths, OperationID, Method, Handler, Acc) + end, #{}, get_operations()). + +build_full_paths([], BasePath, Path) -> + [lists:append([BasePath, Path])]; +build_full_paths(Servers, _BasePath, Path) -> + [lists:append([Server, Path]) || Server <- Servers ]. + +merge_paths(FullPaths, OperationID, Method, Handler, Acc) -> + lists:foldl( + fun(Path, Acc0) -> + case maps:find(Path, Acc0) of + {ok, PathInfo0 = #{operations := Operations0}} -> + Operations = Operations0#{Method => OperationID}, + PathInfo = PathInfo0#{operations => Operations}, + Acc0#{Path => PathInfo}; + error -> + Operations = #{Method => OperationID}, + PathInfo = #{handler => Handler, operations => Operations}, + Acc0#{Path => PathInfo} + end + end, Acc, FullPaths). get_operations() -> #{ - 'ExecutionAddUsersPatch' => #{ + 'addUsers' => #{ + servers => [], + base_path => "", path => "/execution/add_users", method => <<"PATCH">>, handler => 'amoc_rest_execution_handler' }, - 'ExecutionRemoveUsersPatch' => #{ + 'removeUsers' => #{ + servers => [], + base_path => "", path => "/execution/remove_users", method => <<"PATCH">>, handler => 'amoc_rest_execution_handler' }, - 'ExecutionStartPatch' => #{ + 'startScenario' => #{ + servers => [], + base_path => "", path => "/execution/start", method => <<"PATCH">>, handler => 'amoc_rest_execution_handler' }, - 'ExecutionStopPatch' => #{ + 'stopScenario' => #{ + servers => [], + base_path => "", path => "/execution/stop", method => <<"PATCH">>, handler => 'amoc_rest_execution_handler' }, - 'ExecutionUpdateSettingsPatch' => #{ + 'updateSettings' => #{ + servers => [], + base_path => "", path => "/execution/update_settings", method => <<"PATCH">>, handler => 'amoc_rest_execution_handler' }, - 'ScenariosDefaultsIdGet' => #{ - path => "/scenarios/defaults/:id", + 'getAvailableScenarios' => #{ + servers => [], + base_path => "", + path => "/scenarios", method => <<"GET">>, handler => 'amoc_rest_scenarios_handler' }, - 'ScenariosGet' => #{ - path => "/scenarios", + 'getScenarioDescription' => #{ + servers => [], + base_path => "", + path => "/scenarios/info/:id", method => <<"GET">>, handler => 'amoc_rest_scenarios_handler' }, - 'ScenariosInfoIdGet' => #{ - path => "/scenarios/info/:id", + 'getScenarioSettings' => #{ + servers => [], + base_path => "", + path => "/scenarios/defaults/:id", method => <<"GET">>, handler => 'amoc_rest_scenarios_handler' }, - 'ScenariosUploadPut' => #{ + 'uploadNewScenario' => #{ + servers => [], + base_path => "", path => "/scenarios/upload", method => <<"PUT">>, handler => 'amoc_rest_scenarios_handler' }, - 'NodesGet' => #{ - path => "/nodes", + 'getAmocAppStatus' => #{ + servers => [], + base_path => "", + path => "/status", method => <<"GET">>, handler => 'amoc_rest_status_handler' }, - 'StatusGet' => #{ - path => "/status", + 'getAmocAppStatusOnNode' => #{ + servers => [], + base_path => "", + path => "/status/:node", method => <<"GET">>, handler => 'amoc_rest_status_handler' }, - 'StatusNodeGet' => #{ - path => "/status/:node", + 'getClusteredNodes' => #{ + servers => [], + base_path => "", + path => "/nodes", method => <<"GET">>, handler => 'amoc_rest_status_handler' } }. - -prepare_validator() -> - R = jsx:decode(element(2, file:read_file(get_openapi_path()))), - jesse_state:new(R, [{default_schema_ver, <<"http://json-schema.org/draft-04/schema#">>}]). - - -get_openapi_path() -> - {ok, AppName} = application:get_application(?MODULE), - filename:join(amoc_rest_utils:priv_dir(AppName), "openapi.json"). - - diff --git a/src/amoc_rest_scenarios_handler.erl b/src/amoc_rest_scenarios_handler.erl index 48bd6e5..6d28dfc 100644 --- a/src/amoc_rest_scenarios_handler.erl +++ b/src/amoc_rest_scenarios_handler.erl @@ -1,251 +1,161 @@ -%% basic handler -module(amoc_rest_scenarios_handler). +-moduledoc """ +Exposes the following operation IDs: + +- `GET` to `/scenarios`, OperationId: `getAvailableScenarios`: +List all available scenarios. + + +- `GET` to `/scenarios/info/:id`, OperationId: `getScenarioDescription`: +Get scenario description. + + +- `GET` to `/scenarios/defaults/:id`, OperationId: `getScenarioSettings`: +Get scenario default settings. + + +- `PUT` to `/scenarios/upload`, OperationId: `uploadNewScenario`: +Upload a new scenario.. +Uploads new scenario, you can run the next command to upload a file using curl utility `curl -s -H \"Content-Type: text/plain\" -T <filename> http://<amoc_host>/upload` + +""". + +-behaviour(cowboy_rest). + +-include_lib("kernel/include/logger.hrl"). %% Cowboy REST callbacks --export([allowed_methods/2]). -export([init/2]). --export([allow_missing_post/2]). +-export([allowed_methods/2]). -export([content_types_accepted/2]). -export([content_types_provided/2]). -export([delete_resource/2]). -export([is_authorized/2]). --export([known_content_type/2]). --export([malformed_request/2]). -export([valid_content_headers/2]). --export([valid_entity_length/2]). +-export([handle_type_accepted/2, handle_type_provided/2]). -%% Handlers --export([handle_request_json/2]). +-ignore_xref([handle_type_accepted/2, handle_type_provided/2]). --record(state, { - operation_id :: amoc_rest_api:operation_id(), - logic_handler :: atom(), - validator_state :: jesse_state:state(), - context=#{} :: #{} -}). +-export_type([class/0, operation_id/0]). --type state() :: state(). +-type class() :: 'scenarios'. --spec init(Req :: cowboy_req:req(), Opts :: amoc_rest_router:init_opts()) -> - {cowboy_rest, Req :: cowboy_req:req(), State :: state()}. +-type operation_id() :: + 'getAvailableScenarios' %% List all available scenarios + | 'getScenarioDescription' %% Get scenario description + | 'getScenarioSettings' %% Get scenario default settings + | 'uploadNewScenario'. %% Upload a new scenario. -init(Req, {Operations, LogicHandler, ValidatorState}) -> - Method = cowboy_req:method(Req), - OperationID = maps:get(Method, Operations, undefined), - - error_logger:info_msg("Attempt to process operation: ~p", [OperationID]), - State = #state{ - operation_id = OperationID, - logic_handler = LogicHandler, - validator_state = ValidatorState - }, - {cowboy_rest, Req, State}. +-record(state, + {operation_id :: operation_id(), + accept_callback :: amoc_rest_logic_handler:accept_callback(), + provide_callback :: amoc_rest_logic_handler:provide_callback(), + api_key_handler :: amoc_rest_logic_handler:api_key_callback(), + context = #{} :: amoc_rest_logic_handler:context()}). --spec allowed_methods(Req :: cowboy_req:req(), State :: state()) -> - {Value :: [binary()], Req :: cowboy_req:req(), State :: state()}. +-type state() :: #state{}. +-spec init(cowboy_req:req(), amoc_rest_router:init_opts()) -> + {cowboy_rest, cowboy_req:req(), state()}. +init(Req, {Operations, Module}) -> + Method = cowboy_req:method(Req), + OperationID = maps:get(Method, Operations, undefined), + ?LOG_INFO(#{what => "Attempt to process operation", + method => Method, + operation_id => OperationID}), + State = #state{operation_id = OperationID, + accept_callback = fun Module:accept_callback/4, + provide_callback = fun Module:provide_callback/4, + api_key_handler = fun Module:authorize_api_key/2}, + {cowboy_rest, Req, State}. -allowed_methods( - Req, - State = #state{ - operation_id = 'ScenariosDefaultsIdGet' - } -) -> +-spec allowed_methods(cowboy_req:req(), state()) -> + {[binary()], cowboy_req:req(), state()}. +allowed_methods(Req, #state{operation_id = 'getAvailableScenarios'} = State) -> {[<<"GET">>], Req, State}; - -allowed_methods( - Req, - State = #state{ - operation_id = 'ScenariosGet' - } -) -> +allowed_methods(Req, #state{operation_id = 'getScenarioDescription'} = State) -> {[<<"GET">>], Req, State}; - -allowed_methods( - Req, - State = #state{ - operation_id = 'ScenariosInfoIdGet' - } -) -> +allowed_methods(Req, #state{operation_id = 'getScenarioSettings'} = State) -> {[<<"GET">>], Req, State}; - -allowed_methods( - Req, - State = #state{ - operation_id = 'ScenariosUploadPut' - } -) -> +allowed_methods(Req, #state{operation_id = 'uploadNewScenario'} = State) -> {[<<"PUT">>], Req, State}; - allowed_methods(Req, State) -> {[], Req, State}. --spec is_authorized(Req :: cowboy_req:req(), State :: state()) -> - { - Value :: true | {false, AuthHeader :: iodata()}, - Req :: cowboy_req:req(), - State :: state() - }. +-spec is_authorized(cowboy_req:req(), state()) -> + {true | {false, iodata()}, cowboy_req:req(), state()}. is_authorized(Req, State) -> {true, Req, State}. --spec content_types_accepted(Req :: cowboy_req:req(), State :: state()) -> - { - Value :: [{binary(), AcceptResource :: atom()}], - Req :: cowboy_req:req(), - State :: state() - }. - -content_types_accepted(Req, State) -> +-spec content_types_accepted(cowboy_req:req(), state()) -> + {[{binary(), atom()}], cowboy_req:req(), state()}. +content_types_accepted(Req, #state{operation_id = 'getAvailableScenarios'} = State) -> + {[], Req, State}; +content_types_accepted(Req, #state{operation_id = 'getScenarioDescription'} = State) -> + {[], Req, State}; +content_types_accepted(Req, #state{operation_id = 'getScenarioSettings'} = State) -> + {[], Req, State}; +content_types_accepted(Req, #state{operation_id = 'uploadNewScenario'} = State) -> {[ - {<<"application/octet-stream">>, handle_request_json}, - {<<"text/plain">>, handle_request_json} - ], Req, State}. - --spec valid_content_headers(Req :: cowboy_req:req(), State :: state()) -> - {Value :: boolean(), Req :: cowboy_req:req(), State :: state()}. - -valid_content_headers( - Req0, - State = #state{ - operation_id = 'ScenariosDefaultsIdGet' - } -) -> - Headers = [], - {Result, Req} = validate_headers(Headers, Req0), - {Result, Req, State}; - -valid_content_headers( - Req0, - State = #state{ - operation_id = 'ScenariosGet' - } -) -> - Headers = [], - {Result, Req} = validate_headers(Headers, Req0), - {Result, Req, State}; - -valid_content_headers( - Req0, - State = #state{ - operation_id = 'ScenariosInfoIdGet' - } -) -> - Headers = [], - {Result, Req} = validate_headers(Headers, Req0), - {Result, Req, State}; - -valid_content_headers( - Req0, - State = #state{ - operation_id = 'ScenariosUploadPut' - } -) -> - Headers = [], - {Result, Req} = validate_headers(Headers, Req0), - {Result, Req, State}; + {<<"application/octet-stream">>, handle_type_accepted}, + {<<"text/plain">>, handle_type_accepted} + ], Req, State}; +content_types_accepted(Req, State) -> + {[], Req, State}. +-spec valid_content_headers(cowboy_req:req(), state()) -> + {boolean(), cowboy_req:req(), state()}. +valid_content_headers(Req, #state{operation_id = 'getAvailableScenarios'} = State) -> + {true, Req, State}; +valid_content_headers(Req, #state{operation_id = 'getScenarioDescription'} = State) -> + {true, Req, State}; +valid_content_headers(Req, #state{operation_id = 'getScenarioSettings'} = State) -> + {true, Req, State}; +valid_content_headers(Req, #state{operation_id = 'uploadNewScenario'} = State) -> + {true, Req, State}; valid_content_headers(Req, State) -> {false, Req, State}. --spec content_types_provided(Req :: cowboy_req:req(), State :: state()) -> - { - Value :: [{binary(), ProvideResource :: atom()}], - Req :: cowboy_req:req(), - State :: state() - }. - -content_types_provided(Req, State) -> +-spec content_types_provided(cowboy_req:req(), state()) -> + {[{binary(), atom()}], cowboy_req:req(), state()}. +content_types_provided(Req, #state{operation_id = 'getAvailableScenarios'} = State) -> {[ - {<<"application/json">>, handle_request_json} - ], Req, State}. - --spec malformed_request(Req :: cowboy_req:req(), State :: state()) -> - {Value :: false, Req :: cowboy_req:req(), State :: state()}. - -malformed_request(Req, State) -> - {false, Req, State}. - --spec allow_missing_post(Req :: cowboy_req:req(), State :: state()) -> - {Value :: false, Req :: cowboy_req:req(), State :: state()}. - -allow_missing_post(Req, State) -> - {false, Req, State}. - --spec delete_resource(Req :: cowboy_req:req(), State :: state()) -> - processed_response(). + {<<"application/json">>, handle_type_provided} + ], Req, State}; +content_types_provided(Req, #state{operation_id = 'getScenarioDescription'} = State) -> + {[ + {<<"application/json">>, handle_type_provided} + ], Req, State}; +content_types_provided(Req, #state{operation_id = 'getScenarioSettings'} = State) -> + {[ + {<<"application/json">>, handle_type_provided} + ], Req, State}; +content_types_provided(Req, #state{operation_id = 'uploadNewScenario'} = State) -> + {[ + {<<"application/json">>, handle_type_provided} + ], Req, State}; +content_types_provided(Req, State) -> + {[], Req, State}. +-spec delete_resource(cowboy_req:req(), state()) -> + {boolean(), cowboy_req:req(), state()}. delete_resource(Req, State) -> - handle_request_json(Req, State). - --spec known_content_type(Req :: cowboy_req:req(), State :: state()) -> - {Value :: true, Req :: cowboy_req:req(), State :: state()}. - -known_content_type(Req, State) -> - {true, Req, State}. - --spec valid_entity_length(Req :: cowboy_req:req(), State :: state()) -> - {Value :: true, Req :: cowboy_req:req(), State :: state()}. - -valid_entity_length(Req, State) -> - %% @TODO check the length - {true, Req, State}. - -%%%% --type result_ok() :: { - ok, - {Status :: cowboy:http_status(), Headers :: cowboy:http_headers(), Body :: iodata()} -}. - --type result_error() :: {error, Reason :: any()}. - --type processed_response() :: {stop, cowboy_req:req(), state()}. - --spec process_response(result_ok() | result_error(), cowboy_req:req(), state()) -> - processed_response(). - -process_response(Response, Req0, State = #state{operation_id = OperationID}) -> - case Response of - {ok, {Code, Headers, Body}} -> - Req = cowboy_req:reply(Code, Headers, Body, Req0), - {stop, Req, State}; - {error, Message} -> - error_logger:error_msg("Unable to process request for ~p: ~p", [OperationID, Message]), - - Req = cowboy_req:reply(400, Req0), - {stop, Req, State} - end. - --spec handle_request_json(cowboy_req:req(), state()) -> processed_response(). - -handle_request_json( - Req0, - State = #state{ - operation_id = OperationID, - logic_handler = LogicHandler, - validator_state = ValidatorState - } -) -> - case amoc_rest_api:populate_request(OperationID, Req0, ValidatorState) of - {ok, Populated, Req1} -> - {Code, Headers, Body} = amoc_rest_logic_handler:handle_request( - LogicHandler, - OperationID, - Req1, - maps:merge(State#state.context, Populated) - ), - _ = amoc_rest_api:validate_response( - OperationID, - Code, - Body, - ValidatorState - ), - PreparedBody = jsx:encode(Body), - Response = {ok, {Code, Headers, PreparedBody}}, - process_response(Response, Req1, State); - {error, Reason, Req1} -> - process_response({error, Reason}, Req1, State) - end. - -validate_headers(_, Req) -> {true, Req}. + {Res, Req1, State1} = handle_type_accepted(Req, State), + {true =:= Res, Req1, State1}. + +-spec handle_type_accepted(cowboy_req:req(), state()) -> + { amoc_rest_logic_handler:accept_callback_return(), cowboy_req:req(), state()}. +handle_type_accepted(Req, #state{operation_id = OperationID, + accept_callback = Handler, + context = Context} = State) -> + {Res, Req1, Context1} = Handler(scenarios, OperationID, Req, Context), + {Res, Req1, State#state{context = Context1}}. + +-spec handle_type_provided(cowboy_req:req(), state()) -> + { amoc_rest_logic_handler:provide_callback_return(), cowboy_req:req(), state()}. +handle_type_provided(Req, #state{operation_id = OperationID, + provide_callback = Handler, + context = Context} = State) -> + {Res, Req1, Context1} = Handler(scenarios, OperationID, Req, Context), + {Res, Req1, State#state{context = Context1}}. diff --git a/src/amoc_rest_server.erl b/src/amoc_rest_server.erl index 7bcfae2..8d7bccb 100644 --- a/src/amoc_rest_server.erl +++ b/src/amoc_rest_server.erl @@ -1,25 +1,24 @@ -module(amoc_rest_server). +-moduledoc """ +AMOC REST API +""". - --define(DEFAULT_LOGIC_HANDLER, amoc_rest_default_logic_handler). +-define(DEFAULT_LOGIC_HANDLER, amoc_rest_logic_handler). -export([start/2]). - --spec start( ID :: any(), #{ - ip => inet:ip_address(), - port => inet:port_number(), - logic_handler => module(), - net_opts => [] -}) -> {ok, pid()} | {error, any()}. - -start(ID, #{ - ip := IP , - port := Port, - net_opts := NetOpts -} = Params) -> - {Transport, TransportOpts} = get_socket_transport(IP, Port, NetOpts), +-ignore_xref([start/2]). + +-spec start(term(), #{transport => tcp | ssl, + transport_opts => ranch:opts(), + protocol_opts => cowboy:opts(), + logic_handler => module()}) -> + {ok, pid()} | {error, any()}. +start(ID, Params) -> + Transport = maps:get(transport, Params, tcp), + TransportOpts = maps:get(transport_opts, Params, #{}), + ProtocolOpts = maps:get(procotol_opts, Params, #{}), LogicHandler = maps:get(logic_handler, Params, ?DEFAULT_LOGIC_HANDLER), - CowboyOpts = get_default_opts(LogicHandler), + CowboyOpts = get_cowboy_config(LogicHandler, ProtocolOpts), case Transport of ssl -> cowboy:start_tls(ID, TransportOpts, CowboyOpts); @@ -27,25 +26,20 @@ start(ID, #{ cowboy:start_clear(ID, TransportOpts, CowboyOpts) end. -get_socket_transport(IP, Port, Options) -> - Opts = [ - {ip, IP}, - {port, Port} - ], - case amoc_rest_utils:get_opt(ssl, Options) of - SslOpts = [_|_] -> - {ssl, Opts ++ SslOpts}; - undefined -> - {tcp, Opts} - end. +get_cowboy_config(LogicHandler, ExtraOpts) -> + DefaultOpts = get_default_opts(LogicHandler), + maps:fold(fun get_cowboy_config/3, DefaultOpts, ExtraOpts). + +get_cowboy_config(env, #{dispatch := _Dispatch} = Env, AccIn) -> + maps:put(env, Env, AccIn); +get_cowboy_config(env, NewEnv, #{env := OldEnv} = AccIn) -> + Env = maps:merge(OldEnv, NewEnv), + maps:put(env, Env, AccIn); +get_cowboy_config(Key, Value, AccIn) -> + maps:put(Key, Value, AccIn). get_default_dispatch(LogicHandler) -> - [{'_', DefaultPaths}] = amoc_rest_router:get_paths(LogicHandler), - Paths = [{'_', [ %% adding static routing for swagger-ui - {"/api-docs", cowboy_static, {priv_file, amoc_rest, "swagger_ui/index.html"}}, - {"/api-docs/[...]", cowboy_static, {priv_dir, amoc_rest, "swagger_ui"}}, - {"/openapi.json", cowboy_static, {priv_file, amoc_rest, "openapi.json"}} | - DefaultPaths]}], + Paths = amoc_rest_router:get_paths(LogicHandler), #{dispatch => cowboy_router:compile(Paths)}. get_default_opts(LogicHandler) -> diff --git a/src/amoc_rest_status_handler.erl b/src/amoc_rest_status_handler.erl index 474c48f..52c2acc 100644 --- a/src/amoc_rest_status_handler.erl +++ b/src/amoc_rest_status_handler.erl @@ -1,232 +1,143 @@ -%% basic handler -module(amoc_rest_status_handler). +-moduledoc """ +Exposes the following operation IDs: + +- `GET` to `/status`, OperationId: `getAmocAppStatus`: +Get AMOC app status. + + +- `GET` to `/status/:node`, OperationId: `getAmocAppStatusOnNode`: +Get AMOC app status on a remote node. + + +- `GET` to `/nodes`, OperationId: `getClusteredNodes`: +List all AMOC nodes in a cluster.. + + +""". + +-behaviour(cowboy_rest). + +-include_lib("kernel/include/logger.hrl"). %% Cowboy REST callbacks --export([allowed_methods/2]). -export([init/2]). --export([allow_missing_post/2]). +-export([allowed_methods/2]). -export([content_types_accepted/2]). -export([content_types_provided/2]). -export([delete_resource/2]). -export([is_authorized/2]). --export([known_content_type/2]). --export([malformed_request/2]). -export([valid_content_headers/2]). --export([valid_entity_length/2]). +-export([handle_type_accepted/2, handle_type_provided/2]). -%% Handlers --export([handle_request_json/2]). +-ignore_xref([handle_type_accepted/2, handle_type_provided/2]). --record(state, { - operation_id :: amoc_rest_api:operation_id(), - logic_handler :: atom(), - validator_state :: jesse_state:state(), - context=#{} :: #{} -}). +-export_type([class/0, operation_id/0]). --type state() :: state(). +-type class() :: 'status'. --spec init(Req :: cowboy_req:req(), Opts :: amoc_rest_router:init_opts()) -> - {cowboy_rest, Req :: cowboy_req:req(), State :: state()}. +-type operation_id() :: + 'getAmocAppStatus' %% Get AMOC app status + | 'getAmocAppStatusOnNode' %% Get AMOC app status on a remote node + | 'getClusteredNodes'. %% List all AMOC nodes in a cluster. -init(Req, {Operations, LogicHandler, ValidatorState}) -> - Method = cowboy_req:method(Req), - OperationID = maps:get(Method, Operations, undefined), - - error_logger:info_msg("Attempt to process operation: ~p", [OperationID]), - State = #state{ - operation_id = OperationID, - logic_handler = LogicHandler, - validator_state = ValidatorState - }, - {cowboy_rest, Req, State}. +-record(state, + {operation_id :: operation_id(), + accept_callback :: amoc_rest_logic_handler:accept_callback(), + provide_callback :: amoc_rest_logic_handler:provide_callback(), + api_key_handler :: amoc_rest_logic_handler:api_key_callback(), + context = #{} :: amoc_rest_logic_handler:context()}). --spec allowed_methods(Req :: cowboy_req:req(), State :: state()) -> - {Value :: [binary()], Req :: cowboy_req:req(), State :: state()}. +-type state() :: #state{}. +-spec init(cowboy_req:req(), amoc_rest_router:init_opts()) -> + {cowboy_rest, cowboy_req:req(), state()}. +init(Req, {Operations, Module}) -> + Method = cowboy_req:method(Req), + OperationID = maps:get(Method, Operations, undefined), + ?LOG_INFO(#{what => "Attempt to process operation", + method => Method, + operation_id => OperationID}), + State = #state{operation_id = OperationID, + accept_callback = fun Module:accept_callback/4, + provide_callback = fun Module:provide_callback/4, + api_key_handler = fun Module:authorize_api_key/2}, + {cowboy_rest, Req, State}. -allowed_methods( - Req, - State = #state{ - operation_id = 'NodesGet' - } -) -> +-spec allowed_methods(cowboy_req:req(), state()) -> + {[binary()], cowboy_req:req(), state()}. +allowed_methods(Req, #state{operation_id = 'getAmocAppStatus'} = State) -> {[<<"GET">>], Req, State}; - -allowed_methods( - Req, - State = #state{ - operation_id = 'StatusGet' - } -) -> +allowed_methods(Req, #state{operation_id = 'getAmocAppStatusOnNode'} = State) -> {[<<"GET">>], Req, State}; - -allowed_methods( - Req, - State = #state{ - operation_id = 'StatusNodeGet' - } -) -> +allowed_methods(Req, #state{operation_id = 'getClusteredNodes'} = State) -> {[<<"GET">>], Req, State}; - allowed_methods(Req, State) -> {[], Req, State}. --spec is_authorized(Req :: cowboy_req:req(), State :: state()) -> - { - Value :: true | {false, AuthHeader :: iodata()}, - Req :: cowboy_req:req(), - State :: state() - }. +-spec is_authorized(cowboy_req:req(), state()) -> + {true | {false, iodata()}, cowboy_req:req(), state()}. is_authorized(Req, State) -> {true, Req, State}. --spec content_types_accepted(Req :: cowboy_req:req(), State :: state()) -> - { - Value :: [{binary(), AcceptResource :: atom()}], - Req :: cowboy_req:req(), - State :: state() - }. - +-spec content_types_accepted(cowboy_req:req(), state()) -> + {[{binary(), atom()}], cowboy_req:req(), state()}. +content_types_accepted(Req, #state{operation_id = 'getAmocAppStatus'} = State) -> + {[], Req, State}; +content_types_accepted(Req, #state{operation_id = 'getAmocAppStatusOnNode'} = State) -> + {[], Req, State}; +content_types_accepted(Req, #state{operation_id = 'getClusteredNodes'} = State) -> + {[], Req, State}; content_types_accepted(Req, State) -> - {[ - {<<"application/json">>, handle_request_json} - ], Req, State}. - --spec valid_content_headers(Req :: cowboy_req:req(), State :: state()) -> - {Value :: boolean(), Req :: cowboy_req:req(), State :: state()}. - -valid_content_headers( - Req0, - State = #state{ - operation_id = 'NodesGet' - } -) -> - Headers = [], - {Result, Req} = validate_headers(Headers, Req0), - {Result, Req, State}; - -valid_content_headers( - Req0, - State = #state{ - operation_id = 'StatusGet' - } -) -> - Headers = [], - {Result, Req} = validate_headers(Headers, Req0), - {Result, Req, State}; - -valid_content_headers( - Req0, - State = #state{ - operation_id = 'StatusNodeGet' - } -) -> - Headers = [], - {Result, Req} = validate_headers(Headers, Req0), - {Result, Req, State}; + {[], Req, State}. +-spec valid_content_headers(cowboy_req:req(), state()) -> + {boolean(), cowboy_req:req(), state()}. +valid_content_headers(Req, #state{operation_id = 'getAmocAppStatus'} = State) -> + {true, Req, State}; +valid_content_headers(Req, #state{operation_id = 'getAmocAppStatusOnNode'} = State) -> + {true, Req, State}; +valid_content_headers(Req, #state{operation_id = 'getClusteredNodes'} = State) -> + {true, Req, State}; valid_content_headers(Req, State) -> {false, Req, State}. --spec content_types_provided(Req :: cowboy_req:req(), State :: state()) -> - { - Value :: [{binary(), ProvideResource :: atom()}], - Req :: cowboy_req:req(), - State :: state() - }. - -content_types_provided(Req, State) -> +-spec content_types_provided(cowboy_req:req(), state()) -> + {[{binary(), atom()}], cowboy_req:req(), state()}. +content_types_provided(Req, #state{operation_id = 'getAmocAppStatus'} = State) -> {[ - {<<"application/json">>, handle_request_json} - ], Req, State}. - --spec malformed_request(Req :: cowboy_req:req(), State :: state()) -> - {Value :: false, Req :: cowboy_req:req(), State :: state()}. - -malformed_request(Req, State) -> - {false, Req, State}. - --spec allow_missing_post(Req :: cowboy_req:req(), State :: state()) -> - {Value :: false, Req :: cowboy_req:req(), State :: state()}. - -allow_missing_post(Req, State) -> - {false, Req, State}. - --spec delete_resource(Req :: cowboy_req:req(), State :: state()) -> - processed_response(). + {<<"application/json">>, handle_type_provided} + ], Req, State}; +content_types_provided(Req, #state{operation_id = 'getAmocAppStatusOnNode'} = State) -> + {[ + {<<"application/json">>, handle_type_provided} + ], Req, State}; +content_types_provided(Req, #state{operation_id = 'getClusteredNodes'} = State) -> + {[ + {<<"application/json">>, handle_type_provided} + ], Req, State}; +content_types_provided(Req, State) -> + {[], Req, State}. +-spec delete_resource(cowboy_req:req(), state()) -> + {boolean(), cowboy_req:req(), state()}. delete_resource(Req, State) -> - handle_request_json(Req, State). - --spec known_content_type(Req :: cowboy_req:req(), State :: state()) -> - {Value :: true, Req :: cowboy_req:req(), State :: state()}. - -known_content_type(Req, State) -> - {true, Req, State}. - --spec valid_entity_length(Req :: cowboy_req:req(), State :: state()) -> - {Value :: true, Req :: cowboy_req:req(), State :: state()}. - -valid_entity_length(Req, State) -> - %% @TODO check the length - {true, Req, State}. - -%%%% --type result_ok() :: { - ok, - {Status :: cowboy:http_status(), Headers :: cowboy:http_headers(), Body :: iodata()} -}. - --type result_error() :: {error, Reason :: any()}. - --type processed_response() :: {stop, cowboy_req:req(), state()}. - --spec process_response(result_ok() | result_error(), cowboy_req:req(), state()) -> - processed_response(). - -process_response(Response, Req0, State = #state{operation_id = OperationID}) -> - case Response of - {ok, {Code, Headers, Body}} -> - Req = cowboy_req:reply(Code, Headers, Body, Req0), - {stop, Req, State}; - {error, Message} -> - error_logger:error_msg("Unable to process request for ~p: ~p", [OperationID, Message]), - - Req = cowboy_req:reply(400, Req0), - {stop, Req, State} - end. - --spec handle_request_json(cowboy_req:req(), state()) -> processed_response(). - -handle_request_json( - Req0, - State = #state{ - operation_id = OperationID, - logic_handler = LogicHandler, - validator_state = ValidatorState - } -) -> - case amoc_rest_api:populate_request(OperationID, Req0, ValidatorState) of - {ok, Populated, Req1} -> - {Code, Headers, Body} = amoc_rest_logic_handler:handle_request( - LogicHandler, - OperationID, - Req1, - maps:merge(State#state.context, Populated) - ), - _ = amoc_rest_api:validate_response( - OperationID, - Code, - Body, - ValidatorState - ), - PreparedBody = jsx:encode(Body), - Response = {ok, {Code, Headers, PreparedBody}}, - process_response(Response, Req1, State); - {error, Reason, Req1} -> - process_response({error, Reason}, Req1, State) - end. - -validate_headers(_, Req) -> {true, Req}. + {Res, Req1, State1} = handle_type_accepted(Req, State), + {true =:= Res, Req1, State1}. + +-spec handle_type_accepted(cowboy_req:req(), state()) -> + { amoc_rest_logic_handler:accept_callback_return(), cowboy_req:req(), state()}. +handle_type_accepted(Req, #state{operation_id = OperationID, + accept_callback = Handler, + context = Context} = State) -> + {Res, Req1, Context1} = Handler(status, OperationID, Req, Context), + {Res, Req1, State#state{context = Context1}}. + +-spec handle_type_provided(cowboy_req:req(), state()) -> + { amoc_rest_logic_handler:provide_callback_return(), cowboy_req:req(), state()}. +handle_type_provided(Req, #state{operation_id = OperationID, + provide_callback = Handler, + context = Context} = State) -> + {Res, Req1, Context1} = Handler(status, OperationID, Req, Context), + {Res, Req1, State#state{context = Context1}}. diff --git a/src/amoc_rest_utils.erl b/src/amoc_rest_utils.erl deleted file mode 100644 index ef9011a..0000000 --- a/src/amoc_rest_utils.erl +++ /dev/null @@ -1,173 +0,0 @@ --module(amoc_rest_utils). - --export([to_binary/1]). --export([to_list/1]). --export([to_float/1]). --export([to_int/1]). --export([to_lower/1]). --export([to_upper/1]). --export([set_resp_headers/2]). --export([to_header/1]). --export([to_qs/1]). --export([to_binding/1]). --export([get_opt/2]). --export([get_opt/3]). --export([priv_dir/0]). --export([priv_dir/1]). --export([priv_path/1]). - - --spec to_binary(iodata() | atom() | number()) -> binary(). - -to_binary(V) when is_binary(V) -> V; -to_binary(V) when is_list(V) -> iolist_to_binary(V); -to_binary(V) when is_atom(V) -> atom_to_binary(V, utf8); -to_binary(V) when is_integer(V) -> integer_to_binary(V); -to_binary(V) when is_float(V) -> float_to_binary(V). - --spec to_list(iodata() | atom() | number()) -> string(). - -to_list(V) when is_list(V) -> V; -to_list(V) -> binary_to_list(to_binary(V)). - --spec to_float(iodata()) -> number(). - -to_float(V) -> - Data = iolist_to_binary([V]), - case binary:split(Data, <<$.>>) of - [Data] -> - binary_to_integer(Data); - [<<>>, _] -> - binary_to_float(<<$0, Data/binary>>); - _ -> - binary_to_float(Data) - end. - -%% - --spec to_int(integer() | binary() | list()) -> integer(). - -to_int(Data) when is_integer(Data) -> - Data; -to_int(Data) when is_binary(Data) -> - binary_to_integer(Data); -to_int(Data) when is_list(Data) -> - list_to_integer(Data). - --spec set_resp_headers([{binary(), iodata()}], cowboy_req:req()) -> cowboy_req:req(). - -set_resp_headers([], Req) -> - Req; -set_resp_headers([{K, V} | T], Req0) -> - Req = cowboy_req:set_resp_header(K, V, Req0), - set_resp_headers(T, Req). - --spec to_header(iodata() | atom() | number()) -> binary(). - -to_header(Name) -> - Prepared = to_binary(Name), - to_lower(Prepared). - --spec to_qs(iodata() | atom() | number()) -> binary(). - -to_qs(Name) -> - to_binary(Name). - --spec to_binding(iodata() | atom() | number()) -> atom(). - -to_binding(Name) -> - Prepared = to_binary(Name), - binary_to_atom(Prepared, utf8). - --spec get_opt(any(), []) -> any(). - -get_opt(Key, Opts) -> - get_opt(Key, Opts, undefined). - --spec get_opt(any(), [], any()) -> any(). - -get_opt(Key, Opts, Default) -> - case lists:keyfind(Key, 1, Opts) of - {_, Value} -> Value; - false -> Default - end. - --spec priv_dir() -> file:filename(). - -priv_dir() -> - {ok, AppName} = application:get_application(), - priv_dir(AppName). - --spec priv_dir(Application :: atom()) -> file:filename(). - -priv_dir(AppName) -> - case code:priv_dir(AppName) of - Value when is_list(Value) -> - Value ++ "/"; - _Error -> - select_priv_dir([filename:join(["apps", atom_to_list(AppName), "priv"]), "priv"]) - end. - --spec priv_path(Relative :: file:filename()) -> file:filename(). - -priv_path(Relative) -> - filename:join(priv_dir(), Relative). - --include_lib("kernel/include/file.hrl"). - -select_priv_dir(Paths) -> - case lists:dropwhile(fun test_priv_dir/1, Paths) of - [Path | _] -> Path; - _ -> exit(no_priv_dir) - end. - -test_priv_dir(Path) -> - case file:read_file_info(Path) of - {ok, #file_info{type = directory}} -> - false; - _ -> - true - end. - - -%% - --spec to_lower(binary()) -> binary(). - -to_lower(S) -> - to_case(lower, S, <<>>). - --spec to_upper(binary()) -> binary(). - -to_upper(S) -> - to_case(upper, S, <<>>). - -to_case(_Case, <<>>, Acc) -> - Acc; - -to_case(_Case, <>, _Acc) when C > 127 -> - error(badarg); - -to_case(Case = lower, <>, Acc) -> - to_case(Case, Rest, <>); - -to_case(Case = upper, <>, Acc) -> - to_case(Case, Rest, <>). - -to_lower_char(C) when is_integer(C), $A =< C, C =< $Z -> - C + 32; -to_lower_char(C) when is_integer(C), 16#C0 =< C, C =< 16#D6 -> - C + 32; -to_lower_char(C) when is_integer(C), 16#D8 =< C, C =< 16#DE -> - C + 32; -to_lower_char(C) -> - C. - -to_upper_char(C) when is_integer(C), $a =< C, C =< $z -> - C - 32; -to_upper_char(C) when is_integer(C), 16#E0 =< C, C =< 16#F6 -> - C - 32; -to_upper_char(C) when is_integer(C), 16#F8 =< C, C =< 16#FE -> - C - 32; -to_upper_char(C) -> - C.