-
-
Notifications
You must be signed in to change notification settings - Fork 18
Implement basic OpenAPI v3.1 framing support #2799
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
bcc58c2
[WIP] Implement OpenAPI framing support
jviotti 62ecdd3
Better
jviotti 4225348
Fix Windows
jviotti 5d0e0de
Tighten
jviotti 3a80355
Tighten
jviotti 2f26dc8
Fix
jviotti 8a70de2
Fixes
jviotti 85ea11c
Cover 3.1.2
jviotti 8475952
More
jviotti 218b9dd
Fix
jviotti File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| sourcemeta_library(NAMESPACE sourcemeta PROJECT core NAME openapi | ||
| PRIVATE_HEADERS error.h | ||
| SOURCES helpers.h reference.h example.h content.h link.h response.h | ||
| parameter.h request_body.h security.h path_item.h paths.h | ||
| components.h external_documentation.h info.h server.h tag.h | ||
| document.h frame.cc version.cc) | ||
|
|
||
| if(SOURCEMETA_CORE_INSTALL) | ||
| sourcemeta_library_install(NAMESPACE sourcemeta PROJECT core NAME openapi) | ||
| endif() | ||
|
|
||
| target_link_libraries(sourcemeta_core_openapi PUBLIC sourcemeta::core::json) | ||
| target_link_libraries(sourcemeta_core_openapi PUBLIC | ||
| sourcemeta::core::jsonpointer) | ||
| target_link_libraries(sourcemeta_core_openapi PUBLIC sourcemeta::core::memory) | ||
| target_link_libraries(sourcemeta_core_openapi PRIVATE sourcemeta::core::uri) | ||
| target_link_libraries(sourcemeta_core_openapi PRIVATE sourcemeta::core::email) | ||
| target_link_libraries(sourcemeta_core_openapi PRIVATE sourcemeta::core::text) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,181 @@ | ||
| #ifndef SOURCEMETA_CORE_OPENAPI_COMPONENTS_H_ | ||
| #define SOURCEMETA_CORE_OPENAPI_COMPONENTS_H_ | ||
|
|
||
| #include <sourcemeta/core/openapi.h> | ||
|
|
||
| #include "content.h" | ||
| #include "example.h" | ||
| #include "helpers.h" | ||
| #include "link.h" | ||
| #include "parameter.h" | ||
| #include "path_item.h" | ||
| #include "request_body.h" | ||
| #include "response.h" | ||
| #include "security.h" | ||
|
|
||
| #include <array> // std::array | ||
| #include <set> // std::set | ||
| #include <string_view> // std::string_view | ||
|
|
||
| namespace sourcemeta::core { | ||
|
|
||
| constexpr auto OPENAPI_HASH_COMPONENTS{JSON::Object::hash("components"sv)}; | ||
| constexpr auto OPENAPI_HASH_SECURITY_SCHEMES{ | ||
| JSON::Object::hash("securitySchemes"sv)}; | ||
|
|
||
| constexpr std::array<JSON::StringView, 10> OPENAPI_COMPONENTS_FIELDS{ | ||
| {"schemas"sv, "responses"sv, "parameters"sv, "examples"sv, | ||
| "requestBodies"sv, "headers"sv, "securitySchemes"sv, "links"sv, | ||
| "callbacks"sv, "pathItems"sv}}; | ||
|
|
||
| // The names the entry document declares as security schemes, read before the | ||
| // walk goes anywhere so that the order documents are read in cannot decide | ||
| // what a Security Requirement Object may name. This runs before the Components | ||
| // Object has been checked, so it takes what is there and leaves being strict | ||
| // about the shape to that check | ||
| inline auto openapi_collect_security_schemes(const JSON &document, | ||
| OpenAPIWalk &walk) -> void { | ||
| const auto *components{ | ||
| document.try_at("components", OPENAPI_HASH_COMPONENTS)}; | ||
| if (components == nullptr || !components->is_object()) { | ||
| return; | ||
| } | ||
|
|
||
| const auto *schemes{ | ||
| components->try_at("securitySchemes", OPENAPI_HASH_SECURITY_SCHEMES)}; | ||
| if (schemes == nullptr || !schemes->is_object()) { | ||
| return; | ||
| } | ||
|
|
||
| for (const auto &entry : schemes->as_object()) { | ||
| walk.security_schemes.insert(entry.first); | ||
| } | ||
| } | ||
|
|
||
| // OpenAPI Specification 3.1.1, Section 4.8.30: "Lists the required security | ||
| // schemes to execute this operation". Every field is patterned, so unlike | ||
| // almost every other Object this one has no extension carve-out and a member | ||
| // named `x-` is a scheme name | ||
| // OpenAPI Specification 3.1.1, Section 4.8.7: "All the fixed fields declared | ||
| // above are objects that MUST use keys that match the regular expression: | ||
| // `^[a-zA-Z0-9\.\-_]+$`". Every member of that character class is ASCII, so | ||
| // reading the key one byte at a time turns down any other code point too | ||
| inline auto openapi_is_component_key(const JSON::StringView key) noexcept | ||
| -> bool { | ||
| for (const auto character : key) { | ||
| if ((character >= 'a' && character <= 'z') || | ||
| (character >= 'A' && character <= 'Z') || | ||
| (character >= '0' && character <= '9') || character == '.' || | ||
| character == '-' || character == '_') { | ||
| continue; | ||
| } | ||
|
|
||
| return false; | ||
| } | ||
|
|
||
| return !key.empty(); | ||
| } | ||
|
|
||
| // OpenAPI Specification 3.1.1, Section 4.8.7: "Holds a set of reusable objects | ||
| // for different aspects of the OAS". What each entry of those maps holds is | ||
| // not read here, and the Schema Objects under `schemas` are never read at all, | ||
| // as their semantics belong to a JSON Schema implementation | ||
| inline auto openapi_check_components(const JSON &document, OpenAPIWalk &walk) | ||
| -> void { | ||
| const auto *components{ | ||
| document.try_at("components", OPENAPI_HASH_COMPONENTS)}; | ||
| if (components == nullptr) { | ||
| return; | ||
| } | ||
|
|
||
| const Pointer base{"components"}; | ||
| openapi_record(walk, base, OpenAPIObjectKind::Components); | ||
| if (!components->is_object()) { | ||
| throw OpenAPIError{base, "The Components Object must be an object"}; | ||
| } | ||
|
|
||
| openapi_reject_unknown_fields( | ||
| *components, OPENAPI_COMPONENTS_FIELDS, base, | ||
| "The Components Object does not define this field"); | ||
|
|
||
| for (const auto &entry : components->as_object()) { | ||
| if (entry.first.starts_with(OPENAPI_EXTENSION_PREFIX)) { | ||
| continue; | ||
| } | ||
|
|
||
| const auto location{openapi_child(base, entry.first)}; | ||
| if (!entry.second.is_object()) { | ||
| throw OpenAPIError{location, | ||
| "The Components Object fields must each be an object"}; | ||
| } | ||
|
|
||
| // Every entry of every map but one is an Object or a Reference Object, | ||
| // and the specification types both as objects. What sits under `schemas` | ||
| // is a Schema Object, and Section 4.8.24 states that "The empty schema | ||
| // [...] MAY be represented by the boolean value `true` and a schema which | ||
| // allows no instance to validate MAY be represented by the boolean value | ||
| // `false`", which is also all that the meta-schema asserts of a Schema | ||
| // Object position when it leaves those unvalidated | ||
| const auto holds_schemas{entry.first == "schemas"sv}; | ||
|
|
||
| for (const auto &component : entry.second.as_object()) { | ||
| if (component.first.empty()) { | ||
| throw OpenAPIError{openapi_child(location, component.first), | ||
| "The Components Object keys must not be empty"}; | ||
| } | ||
|
|
||
| if (!openapi_is_component_key(component.first)) { | ||
| throw OpenAPIError{openapi_child(location, component.first), | ||
| "The Components Object keys may only hold letters, " | ||
| "digits, dots, hyphens and underscores"}; | ||
| } | ||
|
|
||
| const auto entry_location{openapi_child(location, component.first)}; | ||
| if (holds_schemas) { | ||
| openapi_expect_schema(component.second, entry_location, | ||
| "A Schema Object must be an object or a boolean", | ||
| walk); | ||
| continue; | ||
| } | ||
|
|
||
| openapi_expect_object(component.second, entry_location, | ||
| "The Components Object entries must be objects"); | ||
|
|
||
| if (entry.first == "responses"sv) { | ||
| openapi_check_response_or_reference(component.second, entry_location, | ||
| walk); | ||
| } else if (entry.first == "parameters"sv) { | ||
| if (openapi_is_reference(component.second)) { | ||
| openapi_check_reference(component.second, entry_location, | ||
| OpenAPIObjectKind::Parameter, walk); | ||
| } else { | ||
| [[maybe_unused]] const auto identity{ | ||
| openapi_check_parameter(component.second, entry_location, walk)}; | ||
| } | ||
| } else if (entry.first == "examples"sv) { | ||
| openapi_check_example_or_reference(component.second, entry_location, | ||
| walk); | ||
| } else if (entry.first == "requestBodies"sv) { | ||
| openapi_check_request_body_or_reference(component.second, | ||
| entry_location, walk); | ||
| } else if (entry.first == "headers"sv) { | ||
| openapi_check_header_or_reference(component.second, entry_location, | ||
| walk); | ||
| } else if (entry.first == "securitySchemes"sv) { | ||
| openapi_check_security_scheme_or_reference(component.second, | ||
| entry_location, walk); | ||
| } else if (entry.first == "links"sv) { | ||
| openapi_check_link_or_reference(component.second, entry_location, walk); | ||
| } else if (entry.first == "callbacks"sv) { | ||
| openapi_check_callbacks_or_reference(component.second, entry_location, | ||
| walk); | ||
| } else { | ||
| openapi_check_path_item(component.second, entry_location, walk); | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| } // namespace sourcemeta::core | ||
|
|
||
| #endif |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P2: When a consumer disables any of OpenAPI's dependencies (e.g. -DSOURCEMETA_CORE_JSON=OFF) but leaves the default-on SOURCEMETA_CORE_OPENAPI set, add_subdirectory(src/core/openapi) still runs while src/core/json (and jsonpointer/memory/uri/email) were skipped, so the openapi CMakeLists' target_link_libraries(sourcemeta::core::json) fails configure. Gate the new subdirectory on its library dependencies (or emit a clear error) rather than on SOURCEMETA_CORE_OPENAPI alone.
Prompt for AI agents