Compare commits

...

21 Commits

Author SHA1 Message Date
Jospeh G
40af3ec396 Reference validation using contains() result rather than exception handling 2025-01-22 18:38:26 +01:00
Robert Patterson
8edd521853 check for $defs first, since it is more future-oriented 2025-01-22 18:37:45 +01:00
Robert Patterson
1bcfdf8253 add support for $defs instead of definitions. 2025-01-22 18:37:45 +01:00
ss
fbd72de0d6 Run clang-format
Fix pre-commit workflow failures
2024-09-02 17:13:11 +02:00
Csaba Imre Zempleni
813eb6312b Adding verbose error messages for allOf logical combination 2024-02-07 14:08:25 +01:00
Csaba Imre Zempleni
3a439bdbe9 Adding verbose error messages for logical combinations 2024-02-07 14:08:25 +01:00
Patrick Boettcher
e2f3586dc6 fix-311: add a comment and a detailed test-case 2024-02-05 14:17:12 +01:00
andrejlevkovitch
396ffbb663 fix: issue-311
Fixes #311
2024-02-05 14:17:01 +01:00
bvstrien
0034c11347
Fix cmake install target on windows (#315)
* Fix cmake install target on windows

Install the dll to the bin directory, instead of a non-existent CMAKE_INSTALL_RUNTIMEDIR.

* Combine install targets

The only difference was the `RUNTIME DESTINATION` for windows. Since
that is set to the default path, they can use the same definition.
2024-01-31 11:54:34 +01:00
Cristian Le
dd6b2eda2f
[Temp] Disable clang-tidy (#316)
Signed-off-by: Cristian Le <cristian.le@mpsd.mpg.de>
2024-01-31 11:38:21 +01:00
Patrick Boettcher
08d8a52a8a fix indentation 2023-11-27 12:48:21 +01:00
Patrick Boettcher
74931bd02a error-messages: Numeric limit errors should show maximum precision
Fixes #255
2023-11-27 11:12:23 +01:00
Cristian Le
3f6376dd6a Add simple import tests
Signed-off-by: Cristian Le <cristian.le@mpsd.mpg.de>
2023-11-27 10:26:08 +01:00
Cristian Le
1f0eb98af5 Initialize packit workflow
Signed-off-by: Cristian Le <cristian.le@mpsd.mpg.de>
2023-11-27 10:26:08 +01:00
Jacob Crabill
704a54552d Improve and fix bugs in Conanfile
- Fix issues with Conanfile - create a package which can be consumed by
  downstream Conan packages
- TODO: Add standard Conan workflow instructions to README
2023-11-27 10:24:16 +01:00
Patrick Boettcher
349cba9f7e version 2.3.0
(and CMakeLists whitespace changes)
2023-11-27 10:20:26 +01:00
dhmemi
e3aa397f41
fix: validate multipleOf fails on float-point value (#295)
* fix: validate multipleOf fails on float-point value

* make clang-tidy happy.

* fix test case error when multipleOf is float but number is int

* fix multiple of float number
2023-11-20 13:48:18 +01:00
GerritNG
c6fefb80fb
Update README.md (#301) 2023-10-18 18:55:19 +02:00
andrejlevkovitch
0be4d4c4b5
do not fetch nlohmann_json if it already added in main project (#287) 2023-09-22 16:12:57 +02:00
Sylvain Joubert
79535fe0b6
Fix performance regression from logical combination patch discard (#288)
A recent fix to discard patch from invalid logical combination
introduced a lot (for large json instance and/or schema) of copies to
make a patch backup. On some case, this introduced up to a 100x slower
validation time.

Resizing the patch object to its previous size avoid unnecessary
temporary allocations from the backup object.
2023-09-22 16:03:55 +02:00
ss
2143027c7f
Fix Clang compiler warnings (#290) 2023-09-22 15:59:39 +02:00
33 changed files with 870 additions and 120 deletions

1
.distro/.fmf/version Normal file
View File

@ -0,0 +1 @@
1

4
.distro/.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
/main.fmf
/plans/main.fmf
/tests/main.fmf
*.tar.gz

View File

View File

@ -0,0 +1,61 @@
Name: json-schema-validator
Summary: JSON schema validator for JSON for Modern C++
Version: 0.0.0
Release: %autorelease
License: MIT
URL: https://github.com/pboettch/json-schema-validator
Source: https://github.com/pboettch/json-schema-validator/archive/refs/tags/v%{version}.tar.gz
BuildRequires: ninja-build
BuildRequires: cmake
BuildRequires: gcc-c++
BuildRequires: json-devel
%description
Json schema validator library for C++ projects using nlohmann/json
%package devel
Summary: Development files for JSON schema validator
Requires: json-schema-validator%{?_isa} = %{version}-%{release}
Requires: json-devel
%description devel
Json schema validator development files for C++ projects using nlohmann/json
%prep
%autosetup -n json-schema-validator-%{version}
%build
%cmake \
-DJSON_VALIDATOR_SHARED_LIBS=ON \
-DJSON_VALIDATOR_INSTALL=ON \
-DJSON_VALIDATOR_BUILD_EXAMPLES=OFF \
-DJSON_VALIDATOR_BUILD_TESTS=ON
%cmake_build
%install
%cmake_install
%check
%ctest
%files
%doc README.md
%license LICENSE
%{_libdir}/libnlohmann_json_validator.so.*
%files devel
%{_libdir}/libnlohmann_json_validator.so
%{_includedir}/nlohmann/json-schema.hpp
%{_libdir}/cmake/nlohmann_json_schema_validator
%changelog
%autochangelog

3
.distro/packit.toml Normal file
View File

@ -0,0 +1,3 @@
Filters = [
"unknown-key",
]

12
.distro/plans/import.fmf Normal file
View File

@ -0,0 +1,12 @@
summary:
Basic importing tests
prepare+:
- name: Include minimum fetching packages
how: install
package:
- git
discover+:
how: fmf
filter: "tag: import"
execute:
how: tmt

View File

@ -0,0 +1,4 @@
discover:
how: fmf
dist-git-source: true
path: .distro

14
.distro/plans/rpmlint.fmf Normal file
View File

@ -0,0 +1,14 @@
summary:
Perform rpmlint and rpminspect tests
prepare:
- name: Download the source rpm
how: shell
script: cd /tmp && curl -O ${PACKIT_SRPM_URL}
- name: Download rpm packages
how: shell
script: cd /tmp && dnf download ${PACKIT_COPR_RPMS}
discover+:
how: fmf
filter: "tag: rpmlint"
execute:
how: tmt

7
.distro/plans/smoke.fmf Normal file
View File

@ -0,0 +1,7 @@
summary:
Basic smoke tests
discover+:
how: fmf
filter: "tag: smoke"
execute:
how: tmt

View File

@ -0,0 +1,15 @@
# This is a simple project that tests using cmake to load the installed libraries
cmake_minimum_required(VERSION 3.14)
project(test_fetch_content LANGUAGES CXX)
FetchContent_Declare(nlohmann_json_schema_validator
GIT_REPOSITORY https://github.com/pboettch/json-schema-validator
GIT_TAG main
)
FetchContent_MakeAvailable(nlohmann_json_schema_validator)
if (NOT TARGET nlohmann_json_schema_validator::validator)
message(FATAL_ERROR "Missing target nlohmann_json_schema_validator::validator")
endif ()

View File

@ -0,0 +1,11 @@
# This is a simple project that tests using cmake to load the installed libraries
cmake_minimum_required(VERSION 3.14)
project(test_find_package LANGUAGES CXX)
set(CMAKE_FIND_DEBUG_MODE ON)
find_package(nlohmann_json_schema_validator REQUIRED)
if (NOT TARGET nlohmann_json_schema_validator::validator)
message(FATAL_ERROR "Missing target nlohmann_json_schema_validator::validator")
endif ()

View File

@ -0,0 +1,11 @@
# Common test variables
tag:
- import
tier: 0
path: /tests/import
# Define tests
/find_package:
test: ./test_find_package.sh
/FetchContent:
test: ./test_FetchContent.sh

View File

@ -0,0 +1,4 @@
#!/bin/bash -eux
tmp_dir=$(mktemp -d)
cmake -S ./FetchContent -B ${tmp_dir}

View File

@ -0,0 +1,4 @@
#!/bin/bash -eux
tmp_dir=$(mktemp -d)
cmake -S ./find_package -B ${tmp_dir}

13
.distro/tests/rpmlint.fmf Normal file
View File

@ -0,0 +1,13 @@
# Common test variables
tag:
- rpmlint
tier: 0
path: /
# Define tests
/rpmlint:
summary: Rpmlint spec and rpmfiles
test: rpmlint -c packit.toml -r json-schema-validator.rpmlintrc ./*.spec /tmp/*.rpm
/rpminspect-rpms:
summary: Rpminspect the rpms
test: ls /tmp/*.rpm | xargs -L1 rpminspect-fedora -E metadata,disttag

9
.distro/tests/smoke.fmf Normal file
View File

@ -0,0 +1,9 @@
# Common test variables
tag:
- smoke
tier: 0
path: /
# Define tests
/version:
test: echo "TODO: Write a minimum working example"

1
.gitignore vendored
View File

@ -4,3 +4,4 @@ cmake-build-*
venv venv
env env
compile_commands.json compile_commands.json
.vs/*

79
.packit.yaml Normal file
View File

@ -0,0 +1,79 @@
specfile_path: .distro/json-schema-validator.spec
files_to_sync:
- src: .distro/json-schema-validator.spec
dest: json-schema-validator.spec
- .packit.yaml
- src: .distro/json-schema-validator.rpmlintrc
dest: json-schema-validator.rpmlintrc
# tmt setup
- src: .distro/.fmf/
dest: .fmf/
- src: .distro/plans/
dest: plans/
filters:
- "- .distro/plans/main.fmf.dist-git"
- "- .distro/plans/rpmlint.fmf"
- src: .distro/plans/main.fmf.dist-git
dest: plans/main.fmf
upstream_package_name: json-schema-validator
downstream_package_name: json-schema-validator
update_release: false
upstream_tag_template: v{version}
jobs:
- job: copr_build
trigger: pull_request
owner: lecris
project: json-schema-validator
update_release: true
release_suffix: "{PACKIT_RPMSPEC_RELEASE}"
targets:
- fedora-development
- job: tests
trigger: pull_request
targets:
- fedora-development
fmf_path: .distro
- job: copr_build
trigger: commit
branch: main
owner: lecris
project: nightly
# TODO: Remove when upstream issue is resolved
# https://github.com/packit/packit/issues/1924
additional_repos:
- copr://@scikit-build/release
targets:
- fedora-development-x86_64
- fedora-latest-x86_64
- fedora-development-aarch64
- fedora-latest-aarch64
- job: copr_build
trigger: release
owner: lecris
project: release
targets:
- fedora-development-x86_64
- fedora-latest-x86_64
- fedora-development-aarch64
- fedora-latest-aarch64
- job: tests
trigger: commit
branch: main
targets:
- fedora-development
- fedora-latest
fmf_path: .distro
- job: propose_downstream
trigger: release
dist_git_branches:
- fedora-development
- fedora-latest
- job: koji_build
trigger: commit
dist_git_branches:
- fedora-all
- job: bodhi_update
trigger: commit
dist_git_branches:
- fedora-branched

View File

@ -13,6 +13,7 @@ repos:
- '-Bcmake-build-pre-commit' - '-Bcmake-build-pre-commit'
- '--preset' - '--preset'
- 'pre-commit' - 'pre-commit'
stages: [ manual ]
- repo: https://github.com/pre-commit/pre-commit-hooks - repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0 rev: v4.4.0
hooks: hooks:

View File

@ -10,9 +10,12 @@ endif ()
# Basic project definition # # Basic project definition #
]==============================================================================================] ]==============================================================================================]
# TODO: CMake >= 3.19 can use string(JSON VERSION GET "${METADATA}" "version") to load from JSON
set(PROJECT_VERSION 2.4.0)
# TODO: Version 3, rename the project and namespace to something more compact # TODO: Version 3, rename the project and namespace to something more compact
project(nlohmann_json_schema_validator project(nlohmann_json_schema_validator
VERSION 2.2.0 VERSION ${PROJECT_VERSION}
DESCRIPTION "Json validator for nlohmann::json library" DESCRIPTION "Json validator for nlohmann::json library"
HOMEPAGE_URL "https://github.com/pboettch/json-schema-validator" HOMEPAGE_URL "https://github.com/pboettch/json-schema-validator"
LANGUAGES CXX) LANGUAGES CXX)
@ -80,6 +83,7 @@ endif ()
]==============================================================================================] ]==============================================================================================]
set(fetch_packages "") set(fetch_packages "")
if (NOT TARGET nlohmann_json)
# Fetch/Find nlohmann_json # Fetch/Find nlohmann_json
# TODO: Remove when bumping cmake >= 3.24 # TODO: Remove when bumping cmake >= 3.24
if (CMAKE_VERSION VERSION_GREATER_EQUAL 3.24) if (CMAKE_VERSION VERSION_GREATER_EQUAL 3.24)
@ -101,6 +105,7 @@ else ()
list(APPEND fetch_packages nlohmann_json) list(APPEND fetch_packages nlohmann_json)
endif () endif ()
endif () endif ()
endif ()
# Handle configure flags # Handle configure flags
if (JSON_VALIDATOR_INSTALL) if (JSON_VALIDATOR_INSTALL)

View File

@ -74,7 +74,7 @@ types, depending on if the schema type is "integer" or "number". Bignum
This library is based on Niels Lohmann's JSON-library and thus has This library is based on Niels Lohmann's JSON-library and thus has
a build-dependency to it. a build-dependency to it.
Currently at least version **3.6.0** of NLohmann's JSON library Currently at least version **3.8.0** of NLohmann's JSON library
is required. is required.
Various methods using CMake can be used to build this project. Various methods using CMake can be used to build this project.

View File

@ -1,7 +1,10 @@
import os import os
import re import re
from conans import load, tools, ConanFile, CMake
from conan import ConanFile
from conan.tools.cmake import cmake_layout, CMake, CMakeToolchain
from conans.tools import load
from conans import tools as ctools
def get_version(): def get_version():
try: try:
@ -20,52 +23,59 @@ class JsonSchemaValidatorConan(ConanFile):
version = get_version() version = get_version()
url = 'https://github.com/pboettch/json-schema-validator' url = 'https://github.com/pboettch/json-schema-validator'
license = 'MIT' license = 'MIT'
settings = 'os', 'compiler', 'build_type', 'arch' settings = 'os', 'compiler', 'build_type', 'arch'
options = { options = {
'shared': [True, False], 'shared': [True, False],
'fPIC': [True, False], 'fPIC': [True, False],
'build_examples': [True, False], 'build_examples': [True, False],
'build_tests': [True, False] 'build_tests': [True, False],
'test_coverage': [True, False],
} }
default_options = { default_options = {
'shared': False, 'shared': False,
'fPIC': True, 'fPIC': True,
'build_examples': True, 'build_examples': True,
'build_tests': False 'build_tests': False,
'test_coverage': False,
} }
generators = "CMakeDeps"
generators = 'CMakeDeps', 'CMakeToolchain', 'VirtualBuildEnv', 'VirtualRunEnv'
exports_sources = [ exports_sources = [
'CMakeLists.txt', 'CMakeLists.txt',
'nlohmann_json_schema_validatorConfig.cmake.in', 'conanfile.py',
'cmake/*',
'src/*', 'src/*',
'app/*', 'example/*',
'test/*', 'test/*',
] ]
requires = (
'nlohmann_json/3.11.2'
)
_cmake = None
def _configure_cmake(self): requires = [
if self._cmake: 'nlohmann_json/3.11.2'
return self._cmake ]
self._cmake = CMake(self)
self._cmake.definitions['JSON_VALIDATOR_BUILD_EXAMPLES'] = self.options.build_examples def generate(self):
self._cmake.definitions['JSON_VALIDATOR_BUILD_TESTS'] = self.options.build_tests tc = CMakeToolchain(self)
self._cmake.configure() tc.variables['JSON_VALIDATOR_BUILD_EXAMPLES'] = self.options.build_examples
return self._cmake tc.variables['JSON_VALIDATOR_BUILD_TESTS'] = self.options.build_tests
tc.variables['JSON_VALIDATOR_SHARED_LIBS '] = self.options.shared
tc.variables['JSON_VALIDATOR_TEST_COVERAGE '] = self.options.test_coverage
tc.generate()
def layout(self): def layout(self):
build_type = str(self.settings.build_type).lower() cmake_layout(self)
self.folders.build = "build-{}".format(build_type)
def build(self): def build(self):
cmake = self._configure_cmake() cmake = CMake(self)
cmake.configure() cmake.configure()
cmake.verbose = True
cmake.build() cmake.build()
def package(self): def package(self):
cmake = self._configure_cmake() cmake = CMake(self)
cmake.install() cmake.install()
def package_info(self): def package_info(self):
@ -74,7 +84,7 @@ class JsonSchemaValidatorConan(ConanFile):
libdir = os.path.join(self.package_folder, "lib") libdir = os.path.join(self.package_folder, "lib")
self.cpp_info.libdirs = [libdir] self.cpp_info.libdirs = [libdir]
self.cpp_info.libs += tools.collect_libs(self, libdir) self.cpp_info.libs += ctools.collect_libs(self, libdir)
bindir = os.path.join(self.package_folder, "bin") bindir = os.path.join(self.package_folder, "bin")
self.output.info("Appending PATH environment variable: {}".format(bindir)) self.output.info("Appending PATH environment variable: {}".format(bindir))

View File

@ -54,21 +54,11 @@ target_link_libraries(nlohmann_json_schema_validator PUBLIC
if (JSON_VALIDATOR_INSTALL) if (JSON_VALIDATOR_INSTALL)
# Normal installation target to system. When using scikit-build check python subdirectory # Normal installation target to system. When using scikit-build check python subdirectory
if (WIN32)
# TODO: Probably wrong, please fix
install(TARGETS nlohmann_json_schema_validator install(TARGETS nlohmann_json_schema_validator
EXPORT nlohmann_json_schema_validatorTargets EXPORT nlohmann_json_schema_validatorTargets
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} COMPONENT nlohmann_json_schema_validator_Runtime LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} COMPONENT nlohmann_json_schema_validator_Runtime
NAMELINK_COMPONENT nlohmann_json_schema_validator_Development NAMELINK_COMPONENT nlohmann_json_schema_validator_Development
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} COMPONENT nlohmann_json_schema_validator_Development ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} COMPONENT nlohmann_json_schema_validator_Development
PUBLIC_HEADER DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/nlohmann COMPONENT nlohmann_json_schema_validator_Development PUBLIC_HEADER DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/nlohmann COMPONENT nlohmann_json_schema_validator_Development
RUNTIME DESTINATION ${CMAKE_INSTALL_RUNTIMEDIR} COMPONENT nlohmann_json_schema_validator_Runtime) RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT nlohmann_json_schema_validator_Runtime)
else ()
install(TARGETS nlohmann_json_schema_validator
EXPORT nlohmann_json_schema_validatorTargets
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} COMPONENT nlohmann_json_schema_validator_Runtime
NAMELINK_COMPONENT nlohmann_json_schema_validator_Development
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} COMPONENT nlohmann_json_schema_validator_Development
PUBLIC_HEADER DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/nlohmann COMPONENT nlohmann_json_schema_validator_Development)
endif ()
endif () endif ()

View File

@ -28,10 +28,13 @@ public:
json_patch &replace(const json::json_pointer &, json value); json_patch &replace(const json::json_pointer &, json value);
json_patch &remove(const json::json_pointer &); json_patch &remove(const json::json_pointer &);
json &get_json() { return j_; }
const json &get_json() const { return j_; }
operator json() const { return j_; } operator json() const { return j_; }
private: private:
json j_; json j_ = nlohmann::json::array();
static void validateJsonPatch(json const &patch); static void validateJsonPatch(json const &patch);
}; };

View File

@ -226,10 +226,11 @@ public:
// for each token create an object, if not already existing // for each token create an object, if not already existing
auto unk_kw = &file.unknown_keywords; auto unk_kw = &file.unknown_keywords;
for (auto &rt : ref_tokens) { for (auto &rt : ref_tokens) {
auto existing_object = unk_kw->find(rt); // create a json_pointer from rt as rt can be an stringified integer doing find on an array won't work
if (existing_object == unk_kw->end()) json::json_pointer rt_ptr{"/" + rt};
if (unk_kw->contains(rt_ptr) == false)
(*unk_kw)[rt] = json::object(); (*unk_kw)[rt] = json::object();
unk_kw = &(*unk_kw)[rt]; unk_kw = &(*unk_kw)[rt_ptr];
} }
(*unk_kw)[key] = value; (*unk_kw)[key] = value;
} }
@ -253,15 +254,15 @@ public:
// //
// an unknown keyword can only be referenced by a json-pointer, // an unknown keyword can only be referenced by a json-pointer,
// not by a plain name fragment // not by a plain name fragment
if (uri.pointer().to_string() != "") { if (!uri.pointer().to_string().empty()) {
try { bool contains_pointer = file.unknown_keywords.contains(uri.pointer());
auto &subschema = file.unknown_keywords.at(uri.pointer()); // null is returned if not existing if (contains_pointer) {
auto s = schema::make(subschema, this, {}, {{uri}}); // A JSON Schema MUST be an object or a boolean. auto &subschema = file.unknown_keywords.at(uri.pointer());
if (s) { // nullptr if invalid schema, e.g. null auto s = schema::make(subschema, this, {}, {{uri}});
if (s) { // if schema is valid (non-null)
file.unknown_keywords.erase(uri.fragment()); file.unknown_keywords.erase(uri.fragment());
return s; return s;
} }
} catch (nlohmann::detail::out_of_range &) { // at() did not find it
} }
} }
@ -425,6 +426,31 @@ enum logical_combination_types {
oneOf oneOf
}; };
class logical_combination_error_handler : public error_handler
{
public:
struct error_entry {
json::json_pointer ptr_;
json instance_;
std::string message_;
};
std::vector<error_entry> error_entry_list_;
void error(const json::json_pointer &ptr, const json &instance, const std::string &message) override
{
error_entry_list_.push_back(error_entry{ptr, instance, message});
}
void propagate(error_handler &e, const std::string &prefix) const
{
for (const error_entry &entry : error_entry_list_)
e.error(entry.ptr_, entry.instance_, prefix + entry.message_);
}
operator bool() const { return !error_entry_list_.empty(); }
};
template <enum logical_combination_types combine_logic> template <enum logical_combination_types combine_logic>
class logical_combination : public schema class logical_combination : public schema
{ {
@ -433,29 +459,33 @@ class logical_combination : public schema
void validate(const json::json_pointer &ptr, const json &instance, json_patch &patch, error_handler &e) const final void validate(const json::json_pointer &ptr, const json &instance, json_patch &patch, error_handler &e) const final
{ {
size_t count = 0; size_t count = 0;
logical_combination_error_handler error_summary;
for (auto &s : subschemata_) { for (std::size_t index = 0; index < subschemata_.size(); ++index) {
first_error_handler esub; const std::shared_ptr<schema> &s = subschemata_[index];
json_patch old_patch(patch); logical_combination_error_handler esub;
auto oldPatchSize = patch.get_json().size();
s->validate(ptr, instance, patch, esub); s->validate(ptr, instance, patch, esub);
if (!esub) if (!esub)
count++; count++;
else else {
patch = old_patch; patch.get_json().get_ref<nlohmann::json::array_t &>().resize(oldPatchSize);
esub.propagate(error_summary, "case#" + std::to_string(index) + "] ");
}
if (is_validate_complete(instance, ptr, e, esub, count)) if (is_validate_complete(instance, ptr, e, esub, count, index))
return; return;
} }
// could accumulate esub details for anyOf and oneOf, but not clear how to select which subschema failure to report if (count == 0) {
// or how to report multiple such failures e.error(ptr, instance, "no subschema has succeeded, but one of them is required to validate. Type: " + key + ", number of failed subschemas: " + std::to_string(subschemata_.size()));
if (count == 0) error_summary.propagate(e, "[combination: " + key + " / ");
e.error(ptr, instance, "no subschema has succeeded, but one of them is required to validate"); }
} }
// specialized for each of the logical_combination_types // specialized for each of the logical_combination_types
static const std::string key; static const std::string key;
static bool is_validate_complete(const json &, const json::json_pointer &, error_handler &, const first_error_handler &, size_t); static bool is_validate_complete(const json &, const json::json_pointer &, error_handler &, const logical_combination_error_handler &, size_t, size_t);
public: public:
logical_combination(json &sch, logical_combination(json &sch,
@ -480,21 +510,23 @@ template <>
const std::string logical_combination<oneOf>::key = "oneOf"; const std::string logical_combination<oneOf>::key = "oneOf";
template <> template <>
bool logical_combination<allOf>::is_validate_complete(const json &, const json::json_pointer &, error_handler &e, const first_error_handler &esub, size_t) bool logical_combination<allOf>::is_validate_complete(const json &, const json::json_pointer &, error_handler &e, const logical_combination_error_handler &esub, size_t, size_t current_schema_index)
{ {
if (esub) if (esub) {
e.error(esub.ptr_, esub.instance_, "at least one subschema has failed, but all of them are required to validate - " + esub.message_); e.error(esub.error_entry_list_.front().ptr_, esub.error_entry_list_.front().instance_, "at least one subschema has failed, but all of them are required to validate - " + esub.error_entry_list_.front().message_);
esub.propagate(e, "[combination: allOf / case#" + std::to_string(current_schema_index) + "] ");
}
return esub; return esub;
} }
template <> template <>
bool logical_combination<anyOf>::is_validate_complete(const json &, const json::json_pointer &, error_handler &, const first_error_handler &, size_t count) bool logical_combination<anyOf>::is_validate_complete(const json &, const json::json_pointer &, error_handler &, const logical_combination_error_handler &, size_t count, size_t)
{ {
return count == 1; return count == 1;
} }
template <> template <>
bool logical_combination<oneOf>::is_validate_complete(const json &instance, const json::json_pointer &ptr, error_handler &e, const first_error_handler &, size_t count) bool logical_combination<oneOf>::is_validate_complete(const json &instance, const json::json_pointer &ptr, error_handler &e, const logical_combination_error_handler &, size_t count, size_t)
{ {
if (count > 1) if (count > 1)
e.error(ptr, instance, "more than one subschema has succeeded, but exactly one of them is required to validate"); e.error(ptr, instance, "more than one subschema has succeeded, but exactly one of them is required to validate");
@ -864,7 +896,12 @@ class numeric : public schema
bool violates_multiple_of(T x) const bool violates_multiple_of(T x) const
{ {
double res = std::remainder(x, multipleOf_.second); double res = std::remainder(x, multipleOf_.second);
double multiple = std::fabs(x / multipleOf_.second);
if (multiple > 1) {
res = res / multiple;
}
double eps = std::nextafter(x, 0) - static_cast<double>(x); double eps = std::nextafter(x, 0) - static_cast<double>(x);
return std::fabs(res) > std::fabs(eps); return std::fabs(res) > std::fabs(eps);
} }
@ -872,22 +909,31 @@ class numeric : public schema
{ {
T value = instance; // conversion of json to value_type T value = instance; // conversion of json to value_type
std::ostringstream oss;
if (multipleOf_.first && value != 0) // zero is multiple of everything if (multipleOf_.first && value != 0) // zero is multiple of everything
if (violates_multiple_of(value)) if (violates_multiple_of(value))
e.error(ptr, instance, "instance is not a multiple of " + std::to_string(multipleOf_.second)); oss << "instance is not a multiple of " << json(multipleOf_.second);
if (maximum_.first) { if (maximum_.first) {
if (exclusiveMaximum_ && value >= maximum_.second) if (exclusiveMaximum_ && value >= maximum_.second)
e.error(ptr, instance, "instance exceeds or equals maximum of " + std::to_string(maximum_.second)); oss << "instance exceeds or equals maximum of " << json(maximum_.second);
else if (value > maximum_.second) else if (value > maximum_.second)
e.error(ptr, instance, "instance exceeds maximum of " + std::to_string(maximum_.second)); oss << "instance exceeds maximum of " << json(maximum_.second);
} }
if (minimum_.first) { if (minimum_.first) {
if (exclusiveMinimum_ && value <= minimum_.second) if (exclusiveMinimum_ && value <= minimum_.second)
e.error(ptr, instance, "instance is below or equals minimum of " + std::to_string(minimum_.second)); oss << "instance is below or equals minimum of " << json(minimum_.second);
else if (value < minimum_.second) else if (value < minimum_.second)
e.error(ptr, instance, "instance is below minimum of " + std::to_string(minimum_.second)); oss << "instance is below minimum of " << json(minimum_.second);
}
oss.seekp(0, std::ios::end);
auto size = oss.tellp();
if (size != 0) {
oss.seekp(0, std::ios::beg);
e.error(ptr, instance, oss.str());
} }
} }
@ -1340,11 +1386,18 @@ std::shared_ptr<schema> schema::make(json &schema,
schema.erase(attr); schema.erase(attr);
} }
attr = schema.find("definitions"); auto findDefinitions = [&](const std::string &defs) -> bool {
attr = schema.find(defs);
if (attr != schema.end()) { if (attr != schema.end()) {
for (auto &def : attr.value().items()) for (auto &def : attr.value().items())
schema::make(def.value(), root, {"definitions", def.key()}, uris); schema::make(def.value(), root, {defs, def.key()}, uris);
schema.erase(attr); schema.erase(attr);
return true;
}
return false;
};
if (!findDefinitions("$defs")) {
findDefinitions("definitions");
} }
attr = schema.find("$ref"); attr = schema.find("$ref");

View File

@ -669,10 +669,6 @@ static const short _address_eof_trans[] = {
1278, 1279, 1280, 1281, 1282, 1283, 0}; 1278, 1279, 1280, 1281, 1282, 1283, 0};
static const int address_start = 1; static const int address_start = 1;
static const int address_first_final = 196;
static const int address_error = 0;
static const int address_en_main = 1;
bool is_address(const char *p, const char *pe) bool is_address(const char *p, const char *pe)
{ {

View File

@ -43,6 +43,10 @@ add_executable(issue-98 issue-98.cpp)
target_link_libraries(issue-98 nlohmann_json_schema_validator) target_link_libraries(issue-98 nlohmann_json_schema_validator)
add_test(NAME issue-98-erase-exception-unknown-keywords COMMAND issue-98) add_test(NAME issue-98-erase-exception-unknown-keywords COMMAND issue-98)
add_executable(issue-293 issue-293.cpp)
target_link_libraries(issue-293 nlohmann_json_schema_validator)
add_test(NAME issue-293-float-point-error COMMAND issue-293)
# Unit test for string format checks # Unit test for string format checks
add_executable(string-format-check-test string-format-check-test.cpp) add_executable(string-format-check-test string-format-check-test.cpp)
target_include_directories(string-format-check-test PRIVATE ${PROJECT_SOURCE_DIR}/src/) target_include_directories(string-format-check-test PRIVATE ${PROJECT_SOURCE_DIR}/src/)
@ -81,3 +85,11 @@ add_test(NAME issue-229-oneof-default-values COMMAND issue-229-oneof-default-val
add_executable(issue-243-root-default-values issue-243-root-default-values.cpp) add_executable(issue-243-root-default-values issue-243-root-default-values.cpp)
target_link_libraries(issue-243-root-default-values nlohmann_json_schema_validator) target_link_libraries(issue-243-root-default-values nlohmann_json_schema_validator)
add_test(NAME issue-243-root-default-values COMMAND issue-243-root-default-values) add_test(NAME issue-243-root-default-values COMMAND issue-243-root-default-values)
add_executable(issue-255-error-message-limit-precision issue-255-error-message-limit-precision.cpp)
target_link_libraries(issue-255-error-message-limit-precision nlohmann_json_schema_validator)
add_test(NAME issue-255-error-message-limit-precision COMMAND issue-255-error-message-limit-precision)
add_executable(issue-105-verbose-combination-errors issue-105-verbose-combination-errors.cpp)
target_link_libraries(issue-105-verbose-combination-errors nlohmann_json_schema_validator)
add_test(NAME issue-105-verbose-combination-errors COMMAND issue-105-verbose-combination-errors)

View File

@ -0,0 +1,321 @@
#include "nlohmann/json-schema.hpp"
#include "nlohmann/json.hpp"
#include <iostream>
#include <regex>
#include <string>
#include <vector>
//==============================================================================
// Test macros
//==============================================================================
#define LOG_ERROR(LOG_ERROR__ARGS) \
std::cerr << __FILE__ << ":" << __LINE__ << ": " << LOG_ERROR__ARGS << std::endl
#define EXPECT_THROW_WITH_MESSAGE(EXPRESSION, MESSAGE) \
do { \
try { \
EXPRESSION; \
LOG_ERROR("Expected exception not thrown with matching regex: \"" << MESSAGE << "\""); \
++g_error_count; \
} catch (const std::exception &error) { \
const std::regex error_re{MESSAGE}; \
if (!std::regex_search(error.what(), error_re)) { \
LOG_ERROR("Expected exception with matching regex: \"" << MESSAGE << "\", but got this instead: " << error.what()); \
++g_error_count; \
} \
} \
} while (false)
#define ASSERT_OR_EXPECT_EQ(FIRST_THING, SECOND_THING, RETURN_IN_CASE_OF_ERROR) \
do { \
if ((FIRST_THING) != (SECOND_THING)) { \
LOG_ERROR("The two values of " << (FIRST_THING) << " (" #FIRST_THING << ") and " << (SECOND_THING) << " (" #SECOND_THING << ") should be equal"); \
if (RETURN_IN_CASE_OF_ERROR) { \
return; \
} \
} \
} while (false)
#define ASSERT_EQ(FIRST_THING, SECOND_THING) ASSERT_OR_EXPECT_EQ(FIRST_THING, SECOND_THING, true)
#define EXPECT_EQ(FIRST_THING, SECOND_THING) ASSERT_OR_EXPECT_EQ(FIRST_THING, SECOND_THING, true)
#define EXPECT_MATCH(STRING, REGEX) \
do { \
if (!std::regex_search((STRING), std::regex{(REGEX)})) { \
LOG_ERROR("String \"" << (STRING) << "\" doesn't match with regex: \"" << (REGEX) << "\""); \
++g_error_count; \
} \
} while (false)
namespace
{
//==============================================================================
// Test environment
//==============================================================================
int g_error_count = 0;
//==============================================================================
// The schema used for testing
//==============================================================================
const std::string g_schema_template = R"(
{
"properties": {
"first": {
"%COMBINATION_FIRST_LEVEL%": [
{
"properties": {
"second": {
"%COMBINATION_SECOND_LEVEL%": [
{
"minimum": 5,
"type": "integer"
},
{
"multipleOf": 2,
"type": "integer"
}
]
}
},
"type": "object"
},
{
"minimum": 20,
"type": "integer"
},
{
"minLength": 10,
"type": "string"
}
]
}
},
"type": "object"
}
)";
auto generateSchema(const std::string &first_combination, const std::string &second_combination) -> nlohmann::json
{
static const std::regex first_replace_re{"%COMBINATION_FIRST_LEVEL%"};
static const std::regex second_replace_re{"%COMBINATION_SECOND_LEVEL%"};
std::string intermediate = std::regex_replace(g_schema_template, first_replace_re, first_combination);
return nlohmann::json::parse(std::regex_replace(intermediate, second_replace_re, second_combination));
}
//==============================================================================
// Error handler to catch all the errors generated by the validator - also inside the combinations
//==============================================================================
class MyErrorHandler : public nlohmann::json_schema::error_handler
{
public:
struct ErrorEntry {
nlohmann::json::json_pointer ptr;
nlohmann::json intance;
std::string message;
};
using ErrorEntryList = std::vector<ErrorEntry>;
auto getErrors() const -> const ErrorEntryList &
{
return m_error_list;
}
private:
auto error(const nlohmann::json::json_pointer &ptr, const nlohmann::json &instance, const std::string &message) -> void override
{
m_error_list.push_back(ErrorEntry{ptr, instance, message});
}
ErrorEntryList m_error_list;
};
//==============================================================================
// Error string helpers
//==============================================================================
auto operator<<(std::string first, const std::string &second) -> std::string
{
first += ".*";
first += second;
return first;
}
auto rootError(const std::string &combination_type, std::size_t number_of_subschemas) -> std::string
{
return "no subschema has succeeded, but one of them is required to validate. Type: " + combination_type + ", number of failed subschemas: " + std::to_string(number_of_subschemas);
}
auto combinationError(const std::string &combination_type, std::size_t test_case_number) -> std::string
{
return "[combination: " + combination_type + " / case#" + std::to_string(test_case_number) + "]";
}
//==============================================================================
// Validator function - for simplicity
//==============================================================================
auto validate(const nlohmann::json &schema, const nlohmann::json &instance, nlohmann::json_schema::error_handler *error_handler = nullptr) -> void
{
nlohmann::json_schema::json_validator validator;
validator.set_root_schema(schema);
if (error_handler) {
validator.validate(instance, *error_handler);
} else {
validator.validate(instance);
}
}
//==============================================================================
// The test cases
//==============================================================================
auto simpleTest(const std::string &first_combination, const std::string &second_combination) -> void
{
const nlohmann::json schema = generateSchema(first_combination, second_combination);
EXPECT_THROW_WITH_MESSAGE(validate(schema, nlohmann::json{{"first", {{"second", 1}}}}), rootError(first_combination, 3));
if (second_combination == "oneOf") {
EXPECT_THROW_WITH_MESSAGE(validate(schema, nlohmann::json{{"first", {{"second", 8}}}}), rootError(first_combination, 3));
}
EXPECT_THROW_WITH_MESSAGE(validate(schema, nlohmann::json{{"first", 10}}), rootError(first_combination, 3));
EXPECT_THROW_WITH_MESSAGE(validate(schema, nlohmann::json{{"first", "short"}}), rootError(first_combination, 3));
}
auto verboseTest(const std::string &first_combination, const std::string &second_combination) -> void
{
const nlohmann::json schema = generateSchema(first_combination, second_combination);
{
MyErrorHandler error_handler;
validate(schema, nlohmann::json{{"first", {{"second", 1}}}}, &error_handler);
const MyErrorHandler::ErrorEntryList &error_list = error_handler.getErrors();
EXPECT_EQ(error_list.size(), 6);
EXPECT_EQ(error_list[0].ptr, nlohmann::json::json_pointer{"/first"});
EXPECT_MATCH(error_list[0].message, rootError(first_combination, 3));
EXPECT_EQ(error_list[1].ptr, nlohmann::json::json_pointer{"/first/second"});
EXPECT_MATCH(error_list[1].message, combinationError(first_combination, 0) << rootError(second_combination, 2));
EXPECT_EQ(error_list[2].ptr, nlohmann::json::json_pointer{"/first/second"});
EXPECT_MATCH(error_list[2].message, combinationError(first_combination, 0) << combinationError(second_combination, 0) << "instance is below minimum of 5");
EXPECT_EQ(error_list[3].ptr, nlohmann::json::json_pointer{"/first/second"});
EXPECT_MATCH(error_list[3].message, combinationError(first_combination, 0) << combinationError(second_combination, 1) << "instance is not a multiple of 2.0");
EXPECT_EQ(error_list[4].ptr, nlohmann::json::json_pointer{"/first"});
EXPECT_MATCH(error_list[4].message, combinationError(first_combination, 1) << "unexpected instance type");
EXPECT_EQ(error_list[5].ptr, nlohmann::json::json_pointer{"/first"});
EXPECT_MATCH(error_list[5].message, combinationError(first_combination, 2) << "unexpected instance type");
}
{
MyErrorHandler error_handler;
validate(schema, nlohmann::json{{"first", {{"second", "not-an-integer"}}}}, &error_handler);
const MyErrorHandler::ErrorEntryList &error_list = error_handler.getErrors();
EXPECT_EQ(error_list.size(), 6);
EXPECT_EQ(error_list[0].ptr, nlohmann::json::json_pointer{"/first"});
EXPECT_MATCH(error_list[0].message, rootError(first_combination, 3));
EXPECT_EQ(error_list[1].ptr, nlohmann::json::json_pointer{"/first/second"});
EXPECT_MATCH(error_list[1].message, combinationError(first_combination, 0) << rootError(second_combination, 2));
EXPECT_EQ(error_list[2].ptr, nlohmann::json::json_pointer{"/first/second"});
EXPECT_MATCH(error_list[2].message, combinationError(first_combination, 0) << combinationError(second_combination, 0) << "unexpected instance type");
EXPECT_EQ(error_list[3].ptr, nlohmann::json::json_pointer{"/first/second"});
EXPECT_MATCH(error_list[3].message, combinationError(first_combination, 0) << combinationError(second_combination, 1) << "unexpected instance type");
EXPECT_EQ(error_list[4].ptr, nlohmann::json::json_pointer{"/first"});
EXPECT_MATCH(error_list[4].message, combinationError(first_combination, 1) << "unexpected instance type");
EXPECT_EQ(error_list[5].ptr, nlohmann::json::json_pointer{"/first"});
EXPECT_MATCH(error_list[5].message, combinationError(first_combination, 2) << "unexpected instance type");
}
if (second_combination == "oneOf") {
MyErrorHandler error_handler;
validate(schema, nlohmann::json{{"first", {{"second", 8}}}}, &error_handler);
const MyErrorHandler::ErrorEntryList &error_list = error_handler.getErrors();
EXPECT_EQ(error_list.size(), 4);
EXPECT_EQ(error_list[0].ptr, nlohmann::json::json_pointer{"/first"});
EXPECT_MATCH(error_list[0].message, rootError(first_combination, 3));
EXPECT_EQ(error_list[1].ptr, nlohmann::json::json_pointer{"/first/second"});
EXPECT_MATCH(error_list[1].message, combinationError(first_combination, 0) << "more than one subschema has succeeded, but exactly one of them is required to validate");
EXPECT_EQ(error_list[2].ptr, nlohmann::json::json_pointer{"/first"});
EXPECT_MATCH(error_list[2].message, combinationError(first_combination, 1) << "unexpected instance type");
EXPECT_EQ(error_list[3].ptr, nlohmann::json::json_pointer{"/first"});
EXPECT_MATCH(error_list[3].message, combinationError(first_combination, 2) << "unexpected instance type");
}
{
MyErrorHandler error_handler;
validate(schema, nlohmann::json{{"first", 10}}, &error_handler);
const MyErrorHandler::ErrorEntryList &error_list = error_handler.getErrors();
EXPECT_EQ(error_list.size(), 4);
EXPECT_EQ(error_list[0].ptr, nlohmann::json::json_pointer{"/first"});
EXPECT_MATCH(error_list[0].message, rootError(first_combination, 3));
EXPECT_EQ(error_list[1].ptr, nlohmann::json::json_pointer{"/first"});
EXPECT_MATCH(error_list[1].message, combinationError(first_combination, 0) << "unexpected instance type");
EXPECT_EQ(error_list[2].ptr, nlohmann::json::json_pointer{"/first"});
EXPECT_MATCH(error_list[2].message, combinationError(first_combination, 1) << "instance is below minimum of 20");
EXPECT_EQ(error_list[3].ptr, nlohmann::json::json_pointer{"/first"});
EXPECT_MATCH(error_list[3].message, combinationError(first_combination, 2) << "unexpected instance type");
}
{
MyErrorHandler error_handler;
validate(schema, nlohmann::json{{"first", "short"}}, &error_handler);
const MyErrorHandler::ErrorEntryList &error_list = error_handler.getErrors();
EXPECT_EQ(error_list.size(), 4);
EXPECT_EQ(error_list[0].ptr, nlohmann::json::json_pointer{"/first"});
EXPECT_MATCH(error_list[0].message, rootError(first_combination, 3));
EXPECT_EQ(error_list[1].ptr, nlohmann::json::json_pointer{"/first"});
EXPECT_MATCH(error_list[1].message, combinationError(first_combination, 0) << "unexpected instance type");
EXPECT_EQ(error_list[2].ptr, nlohmann::json::json_pointer{"/first"});
EXPECT_MATCH(error_list[2].message, combinationError(first_combination, 1) << "unexpected instance type");
EXPECT_EQ(error_list[3].ptr, nlohmann::json::json_pointer{"/first"});
EXPECT_MATCH(error_list[3].message, combinationError(first_combination, 2) << "instance is too short as per minLength:10");
}
}
} // namespace
//==============================================================================
// MAIN - calling the test cases
//==============================================================================
auto main() -> int
{
simpleTest("anyOf", "anyOf");
simpleTest("anyOf", "oneOf");
simpleTest("oneOf", "anyOf");
simpleTest("oneOf", "oneOf");
verboseTest("anyOf", "anyOf");
verboseTest("anyOf", "oneOf");
verboseTest("oneOf", "anyOf");
verboseTest("oneOf", "oneOf");
return g_error_count;
}

View File

@ -0,0 +1,41 @@
#include <nlohmann/json-schema.hpp>
#include <stdexcept>
using nlohmann::json;
using nlohmann::json_schema::json_validator;
static const json schema = R"(
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "arc.schema.json",
"properties": {
"angle": {
"type": "number",
"description": "Radians, from -π to π.",
"minimum": -3.14159265358979323846,
"maximum": 3.14159265358979323846
}
}
})"_json;
class custom_error_handler : public nlohmann::json_schema::basic_error_handler
{
void error(const nlohmann::json::json_pointer &ptr, const json &instance, const std::string &message) override
{
if (message != "instance exceeds maximum of 3.141592653589793")
throw std::invalid_argument("Precision print does not work.");
}
};
int main(void)
{
json_validator validator;
auto instance = R"({ "angle": 3.1415927410125732 })"_json;
validator.set_root_schema(schema);
custom_error_handler err;
validator.validate(instance, err);
return 0;
}

32
test/issue-293.cpp Normal file
View File

@ -0,0 +1,32 @@
#include "nlohmann/json-schema.hpp"
using nlohmann::json_schema::json_validator;
template <typename T>
int should_throw(const nlohmann::json &schema, T value)
{
try {
json_validator(schema).validate(value);
} catch (const std::exception &ex) {
return 0;
}
return 1;
}
int main(void)
{
json_validator({{"type", "number"}, {"multipleOf", 0.001}}).validate(0.3 - 0.2);
json_validator({{"type", "number"}, {"multipleOf", 3.3}}).validate(8.0 - 1.4);
json_validator({{"type", "number"}, {"multipleOf", 1000.01}}).validate((1000.03 - 0.02) * 15.0);
json_validator({{"type", "number"}, {"multipleOf", 0.001}}).validate(0.030999999999999993);
json_validator({{"type", "number"}, {"multipleOf", 0.100000}}).validate(1.9);
json_validator({{"type", "number"}, {"multipleOf", 100000.1}}).validate(9000009);
int exc_count = 0;
exc_count += should_throw({{"type", "number"}, {"multipleOf", 0.001}}, 0.3 - 0.2005);
exc_count += should_throw({{"type", "number"}, {"multipleOf", 1000.02}}, (1000.03 - 0.02) * 15.0);
exc_count += should_throw({{"type", "number"}, {"multipleOf", 100000.11}}, 9000009);
return exc_count;
}

View File

@ -0,0 +1,3 @@
add_test_simple_schema(Issue::311
${CMAKE_CURRENT_SOURCE_DIR}/schema.json
${CMAKE_CURRENT_SOURCE_DIR}/instance.json)

View File

@ -0,0 +1,4 @@
{
"element": [1],
"element2": "test"
}

View File

@ -0,0 +1,26 @@
{
"type": "object",
"properties": {
"element": {
"$ref": "#/$defs/element"
},
"element2": {
"$ref": "#/$defs/element/items/0/$defs/element2"
}
},
"$defs": {
"element": {
"type": "array",
"items": [
{
"$defs": {
"element2": {
"type": "string"
}
},
"type": "number"
}
]
}
}
}