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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions docs/bundle.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,19 @@ two spaces. Use `--indentation/-n` to set the width in either case.
> implementations like Visual Studio Code, and avoid using the resulting schema
> in any other way.

OpenAPI Descriptions
--------------------

If the input is an OpenAPI spec v3.1 or v3.2, the `bundle` command treats it as
an [OpenAPI](https://spec.openapis.org/oas/latest.html) description rather than
as a schema, and bundles it as one: every other document the description spans
is embedded into the Components Object, and every reference that reached one is
rewritten to name where it landed. Any other revision, such as v3.0, is
rejected rather than read as a schema.

Use `--resolve/-r` for the other documents of the description exactly as you
would for schemas.
Comment thread
jviotti marked this conversation as resolved.

Examples
--------

Expand Down Expand Up @@ -147,3 +160,15 @@ jsonschema bundle path/to/my/schema.json \
```sh
jsonschema bundle path/to/my/schema.json --resolve path/to/external.json --without-id
```

### Bundle an OpenAPI description split across other documents

```sh
jsonschema bundle path/to/my/openapi.json
```

### Bundle an OpenAPI description importing a single local schema

```sh
jsonschema bundle path/to/my/openapi.json --resolve path/to/external.json
```
62 changes: 60 additions & 2 deletions src/command_bundle.cc
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,6 @@ auto sourcemeta::jsonschema::bundle(const sourcemeta::core::Options &options)

const auto schema_config_base{
schema_from_stdin ? std::filesystem::current_path() : schema_path};
const auto schema_display_path{schema_from_stdin ? stdin_path()
: schema_path};

const auto configuration_path{
find_configuration(options, schema_config_base)};
Expand All @@ -45,6 +43,16 @@ auto sourcemeta::jsonschema::bundle(const sourcemeta::core::Options &options)
? read_from_stdin(nullptr, InputFormatting::Preserve)
: read_file(schema_path, InputFormatting::Preserve)};

// An OpenAPI description is not a schema, so what it goes by wherever we
// report on it is an identity of its own rather than the one a schema from
// the same place would take
const auto is_description{is_openapi_document(parsed_schema.document)};
const auto is_openapi{
sourcemeta::core::openapi_version(parsed_schema.document).has_value()};
const auto schema_display_path{
schema_from_stdin ? (is_description ? openapi_stdin_path() : stdin_path())
: schema_path};

if (parsed_schema.multidocument) {
throw MultiDocumentInputError{
"This command does not support input with multiple documents",
Expand All @@ -58,12 +66,41 @@ auto sourcemeta::jsonschema::bundle(const sourcemeta::core::Options &options)

reject_unsupported_openapi(parsed_schema.document, schema_display_path);

// Removing identifiers is a schema operation, and there is no description to
// read once the Schema Objects it holds can no longer say who they are
if (is_openapi && options.contains("without-id")) {
throw sourcemeta::core::FileError<UnsupportedOpenAPIWithoutIdError>(
schema_display_path);
}

auto &schema{parsed_schema.document};

const auto openapi_base{sourcemeta::jsonschema::openapi_default_id(
schema_path, schema_from_stdin)};
// A description that names itself with `$self` answers to that rather than to
// where it was read from, and a fault in it reports that base, so both are
// what tells a fault of ours from one in a document bundling went and fetched
const auto openapi_self{
openapi_self_identity(parsed_schema.document, openapi_base)};

const auto &custom_resolver{
resolver(options, options.contains("http"), dialect, configuration)};

try {
if (is_openapi) {
sourcemeta::core::openapi_bundle(
schema, sourcemeta::core::schema_walker, custom_resolver,
openapi_resolver(options, options.contains("http"), dialect,
configuration),
{.default_base = openapi_base});
// TODO: Order the keys of a bundled description once Core grows the
// equivalent of `sourcemeta::core::schema_format` for one. The shell of
// a description has no ordering of its own to apply here
sourcemeta::jsonschema::write_schema(schema, std::cout, indentation,
parsed_schema.roundtrip);
return;
}

sourcemeta::core::schema_bundle(
schema, sourcemeta::core::schema_walker, custom_resolver, dialect,
sourcemeta::jsonschema::default_id(schema_path, schema_from_stdin));
Expand All @@ -87,6 +124,27 @@ auto sourcemeta::jsonschema::bundle(const sourcemeta::core::Options &options)
}

sourcemeta::jsonschema::format_schema(schema, custom_resolver, dialect);
} catch (const sourcemeta::core::OpenAPIResolutionError &error) {
throw sourcemeta::core::FileError<sourcemeta::core::OpenAPIResolutionError>(
schema_display_path, error);
} catch (const sourcemeta::core::OpenAPIReferenceError &error) {
throw sourcemeta::core::FileError<sourcemeta::core::OpenAPIReferenceError>(
schema_display_path, error);
} catch (const sourcemeta::core::OpenAPIError &error) {
const auto ours{
error.base() == openapi_base ||
(openapi_self.has_value() && error.base() == openapi_self.value())};
const auto position{ours ? parsed_schema.positions.get(error.location())
: std::nullopt};
if (position.has_value()) {
throw PositionError<
sourcemeta::core::FileError<sourcemeta::core::OpenAPIError>>(
std::get<0>(position.value()), std::get<1>(position.value()),
schema_display_path, error);
}

throw sourcemeta::core::FileError<sourcemeta::core::OpenAPIError>(
schema_display_path, error);
} catch (const sourcemeta::core::SchemaKeywordError &error) {
throw sourcemeta::core::FileError<sourcemeta::core::SchemaKeywordError>(
schema_display_path, error);
Expand Down
17 changes: 3 additions & 14 deletions src/command_lint.cc
Original file line number Diff line number Diff line change
Expand Up @@ -170,25 +170,14 @@ static auto get_lint_callback(sourcemeta::core::JSON &errors_array,
};
}

// An OpenAPI description declares no identifier of its own under the revisions
// we support, so the one it is linted under is where it came from
static auto openapi_default_id(const sourcemeta::jsonschema::InputJSON &entry)
-> std::string {
if (entry.from_stdin) {
return std::string{sourcemeta::jsonschema::STDIN_OPENAPI_DEFAULT_ID};
}

return sourcemeta::jsonschema::default_id(entry);
}

// An OpenAPI description that comes from standard input is not a schema, so it
// goes by an identifier of its own wherever we report on it
static auto
retag_openapi_stdin(std::vector<sourcemeta::jsonschema::InputJSON> &entries)
-> void {
for (auto &entry : entries) {
if (entry.from_stdin &&
sourcemeta::core::openapi_version(entry.second).has_value()) {
sourcemeta::jsonschema::is_openapi_document(entry.second)) {
entry.first =
std::string{sourcemeta::jsonschema::STDIN_OPENAPI_DEFAULT_ID};
entry.resolution_base = sourcemeta::jsonschema::openapi_stdin_path();
Expand All @@ -204,7 +193,7 @@ check_openapi(const sourcemeta::blaze::SchemaTransformer &bundle,
-> std::pair<bool, std::uint8_t> {
const sourcemeta::core::OpenAPIFrame frame{
entry.second, sourcemeta::core::schema_walker, resolver,
openapi_default_id(entry)};
sourcemeta::jsonschema::openapi_default_id(entry)};
return bundle.check(entry.second, frame.schemas(),
sourcemeta::core::schema_walker, resolver, callback,
sourcemeta::core::JSON::String{EXCLUDE_KEYWORD});
Expand All @@ -217,7 +206,7 @@ apply_openapi(const sourcemeta::blaze::SchemaTransformer &bundle,
const sourcemeta::core::SchemaResolver &resolver,
const sourcemeta::blaze::SchemaTransformer::Callback &callback)
-> std::pair<bool, std::uint8_t> {
const auto default_base{openapi_default_id(entry)};
const auto default_base{sourcemeta::jsonschema::openapi_default_id(entry)};
std::optional<sourcemeta::core::OpenAPIFrame> frame;
return bundle.apply(
document,
Expand Down
68 changes: 67 additions & 1 deletion src/error.h
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,29 @@ class SchemaIdentifierConflictError : public std::runtime_error {
sourcemeta::core::Pointer other_;
};

class OpenAPIIdentifierConflictError : public std::runtime_error {
public:
OpenAPIIdentifierConflictError(std::string identifier,
std::filesystem::path other_path)
: std::runtime_error{"Conflicting OpenAPI descriptions for the same "
"identifier"},
identifier_{std::move(identifier)}, other_path_{std::move(other_path)} {
}

[[nodiscard]] auto identifier() const noexcept -> const std::string & {
return this->identifier_;
}

[[nodiscard]] auto other_path() const noexcept
-> const std::filesystem::path & {
return this->other_path_;
}

private:
std::string identifier_;
std::filesystem::path other_path_;
};

class PositionalArgumentError : public std::runtime_error {
public:
PositionalArgumentError(const std::string &message, std::string example)
Expand Down Expand Up @@ -178,6 +201,13 @@ class UnsupportedOpenAPIFormatError : public std::runtime_error {
"The --format option is not supported for OpenAPI descriptions"} {}
};

class UnsupportedOpenAPIWithoutIdError : public std::runtime_error {
public:
UnsupportedOpenAPIWithoutIdError()
: std::runtime_error{"The --without-id option is not supported for "
"OpenAPI descriptions"} {}
};

class UnsupportedOpenAPIVersionError : public std::runtime_error {
public:
UnsupportedOpenAPIVersionError(std::string value)
Expand Down Expand Up @@ -1034,6 +1064,20 @@ inline auto print_exception(const bool is_json, const Exception &exception)
}
}

// An OpenAPI error names which document of a description is at fault, which
// is not always the one the command was handed, and it spells that as a URI
// rather than holding one
if constexpr (requires(const Exception &current) {
{ current.base() } -> std::convertible_to<std::string_view>;
}) {
if (is_json) {
error_json.assign("baseURI",
sourcemeta::core::JSON{std::string{exception.base()}});
} else {
std::cerr << " at base uri " << exception.base() << "\n";
}
}

if constexpr (requires(const Exception &current) {
current.base().recompose();
}) {
Expand Down Expand Up @@ -1195,6 +1239,11 @@ inline auto try_catch(const sourcemeta::core::Options &options,
const auto is_json{options.contains("json")};
print_exception(is_json, error);
return EXIT_NOT_SUPPORTED;
} catch (const sourcemeta::core::FileError<UnsupportedOpenAPIWithoutIdError>
&error) {
const auto is_json{options.contains("json")};
print_exception(is_json, error);
return EXIT_NOT_SUPPORTED;
} catch (const UnsupportedDialectCodegenError &error) {
const auto is_json{options.contains("json")};
print_exception(is_json, error);
Expand Down Expand Up @@ -1511,7 +1560,8 @@ inline auto try_catch(const sourcemeta::core::Options &options,
print_exception(is_json, error);
if (!is_json) {
if (error.identifier().starts_with("file://")) {
std::cerr << "\nThis is likely because the file does not exist\n";
std::cerr << "\nThis is likely because the file does not exist, or "
"does not hold a JSON Schema\n";
} else {
std::cerr
<< "\nThis is likely because you forgot to import such schema "
Expand Down Expand Up @@ -1571,6 +1621,11 @@ inline auto try_catch(const sourcemeta::core::Options &options,
const auto is_json{options.contains("json")};
print_exception(is_json, error);
return EXIT_SCHEMA_INPUT_ERROR;
} catch (const sourcemeta::core::FileError<OpenAPIIdentifierConflictError>
&error) {
const auto is_json{options.contains("json")};
print_exception(is_json, error);
return EXIT_SCHEMA_INPUT_ERROR;
} catch (const PositionError<sourcemeta::core::FileError<
sourcemeta::core::SchemaAnchorCollisionError>> &error) {
const auto is_json{options.contains("json")};
Expand All @@ -1592,6 +1647,17 @@ inline auto try_catch(const sourcemeta::core::Options &options,
const auto is_json{options.contains("json")};
print_exception(is_json, error);
return EXIT_SCHEMA_INPUT_ERROR;
} catch (const sourcemeta::core::FileError<
sourcemeta::core::OpenAPIResolutionError> &error) {
const auto is_json{options.contains("json")};
print_exception(is_json, error);
return EXIT_SCHEMA_INPUT_ERROR;
} catch (
const sourcemeta::core::FileError<sourcemeta::core::OpenAPIReferenceError>
&error) {
const auto is_json{options.contains("json")};
print_exception(is_json, error);
return EXIT_SCHEMA_INPUT_ERROR;
} catch (const sourcemeta::core::FileError<sourcemeta::core::SchemaFrameError>
&error) {
const auto is_json{options.contains("json")};
Expand Down
Loading
Loading