Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
9 changes: 9 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ option(SOURCEMETA_CORE_JSONRPC "Build the Sourcemeta Core JSON-RPC library" ON)
option(SOURCEMETA_CORE_MCP "Build the Sourcemeta Core MCP library" ON)
option(SOURCEMETA_CORE_HTTP "Build the Sourcemeta Core HTTP library" ON)
option(SOURCEMETA_CORE_HTTP_USE_SYSTEM_CURL "Use system cURL for the Sourcemeta Core HTTP library" OFF)
option(SOURCEMETA_CORE_OPENAPI "Build the Sourcemeta Core OpenAPI library" ON)
option(SOURCEMETA_CORE_JOSE "Build the Sourcemeta Core JOSE library" ON)
option(SOURCEMETA_CORE_OAUTH "Build the Sourcemeta Core OAuth library" ON)
option(SOURCEMETA_CORE_OIDC "Build the Sourcemeta Core OIDC library" ON)
Expand Down Expand Up @@ -251,6 +252,10 @@ if(SOURCEMETA_CORE_HTTP)
add_subdirectory(src/core/http)
endif()

if(SOURCEMETA_CORE_OPENAPI)
add_subdirectory(src/core/openapi)

Copy link
Copy Markdown

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
Check if this issue is valid — if so, understand the root cause and fix it. At CMakeLists.txt, line 256:

<comment>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.</comment>

<file context>
@@ -251,6 +252,10 @@ if(SOURCEMETA_CORE_HTTP)
 endif()
 
+if(SOURCEMETA_CORE_OPENAPI)
+  add_subdirectory(src/core/openapi)
+endif()
+
</file context>

endif()

if(SOURCEMETA_CORE_JOSE)
add_subdirectory(src/core/jose)
endif()
Expand Down Expand Up @@ -447,6 +452,10 @@ if(SOURCEMETA_CORE_TESTS)
add_subdirectory(test/http)
endif()

if(SOURCEMETA_CORE_OPENAPI)
add_subdirectory(test/openapi)
endif()

if(SOURCEMETA_CORE_JOSE)
add_subdirectory(test/jose)
endif()
Expand Down
1 change: 1 addition & 0 deletions DEPENDENCIES
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ jsonschema-draft2 https://github.com/json-schema-org/json-schema-spec 707f65070d
jsonschema-draft1 https://github.com/json-schema-org/json-schema-spec 2072feec9fc7a7ff0b2bb5b02c2d6742c554cc4a
jsonschema-draft0 https://github.com/json-schema-org/json-schema-spec 7ea575aef8d5c0183acbe6ff65b4c98ee9c236ec
openapi https://github.com/OAI/OpenAPI-Specification 74906beddddab9e555337031b2a8d8e9338c4972
openapi-test-suite-3-1 https://github.com/OAI/OpenAPI-Specification 8df69dd6b8c12f50c99e28a864e500aadf3394e5
referencing-suite https://github.com/python-jsonschema/referencing-suite 61c4cc202b1e96ed5adcaf4842a595f68d659212
iana-oauth/parameters.csv https://www.iana.org/assignments/oauth-parameters/parameters.csv acfe19a091a15279587e761a399704e886447eec906b19a41528cb37dc2afc35
iana-oauth/extensions-error.csv https://www.iana.org/assignments/oauth-parameters/extensions-error.csv b49d3e1c9904667551170d12d0943ae9613751f42581f6438a4714c774d64838
Expand Down
18 changes: 18 additions & 0 deletions config.cmake.in
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ if(NOT SOURCEMETA_CORE_COMPONENTS)
list(APPEND SOURCEMETA_CORE_COMPONENTS jsonrpc)
list(APPEND SOURCEMETA_CORE_COMPONENTS mcp)
list(APPEND SOURCEMETA_CORE_COMPONENTS http)
list(APPEND SOURCEMETA_CORE_COMPONENTS openapi)
list(APPEND SOURCEMETA_CORE_COMPONENTS jose)
list(APPEND SOURCEMETA_CORE_COMPONENTS oauth)
list(APPEND SOURCEMETA_CORE_COMPONENTS oidc)
Expand Down Expand Up @@ -249,6 +250,23 @@ foreach(component ${SOURCEMETA_CORE_COMPONENTS})
include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_ip.cmake")
include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_uri.cmake")
include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_http.cmake")
elseif(component STREQUAL "openapi")
include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_io.cmake")
include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_preprocessor.cmake")
include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_numeric.cmake")
include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_text.cmake")
include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_unicode.cmake")
include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_json.cmake")
include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_regex.cmake")
include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_ip.cmake")
include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_uri.cmake")
include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_jsonpointer.cmake")
include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_memory.cmake")
include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_punycode.cmake")
include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_idna.cmake")
include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_dns.cmake")
include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_email.cmake")
include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_openapi.cmake")
elseif(component STREQUAL "jose")
if(@SOURCEMETA_CORE_CRYPTO_USE_SYSTEM_OPENSSL@)
find_dependency(OpenSSL 3.0)
Expand Down
18 changes: 18 additions & 0 deletions src/core/openapi/CMakeLists.txt
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)
181 changes: 181 additions & 0 deletions src/core/openapi/components.h
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
Loading