Skip to content
This repository has been archived by the owner on Aug 21, 2024. It is now read-only.

anyOf #19

Draft
wants to merge 22 commits into
base: merging
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
574bac0
Add merge testing infrastructure
patrickkwang Feb 19, 2021
af2f72a
Add simple, very dumb schema merger
patrickkwang Feb 19, 2021
d282afb
isort
patrickkwang Feb 19, 2021
cedf0c9
Skip testing test files with isort
patrickkwang Feb 19, 2021
fe3fb62
Added a more in depth recursive merging function
alongreyber Feb 19, 2021
23d6cf9
Added a test to ensure merging doesn't modify parameters
alongreyber Feb 19, 2021
9371afe
Added test for merging conflicting values
alongreyber Feb 19, 2021
02fdefd
Added test for conflicting with update
alongreyber Feb 19, 2021
652243d
Removed update parameter for merge
alongreyber Feb 19, 2021
aa893eb
Restructured to reduce nesting
alongreyber Feb 19, 2021
552a16c
Fixed for python 3.6
alongreyber Feb 19, 2021
c80cbce
Rename variables to make the linter happy
patrickkwang Feb 21, 2021
118277b
Shorten too-long lines and standardize docstrings
patrickkwang Feb 21, 2021
eb9fb49
Fixed issue with recursive merging and deepcopy
alongreyber Feb 22, 2021
e9d4fa8
Modified merge signature to specifically switch to in place merging f…
alongreyber Feb 22, 2021
4a68e0c
Added allOf support to the schema
alongreyber Feb 18, 2021
b45a141
Moved allOf handling to generate_json method
alongreyber Feb 22, 2021
90c7028
Add test for anyOf
patrickkwang Feb 18, 2021
0f75400
Add simple support for anyOf
patrickkwang Feb 18, 2021
52b1011
Catch another not-implemented area
patrickkwang Feb 18, 2021
dafc037
Added sampling of anyOf schemas
alongreyber Feb 22, 2021
2d0c69f
Fixed bug with in place merge_list
alongreyber Feb 22, 2021
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
1 change: 1 addition & 0 deletions .github/workflows/linting.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,5 @@ jobs:
VALIDATE_PYTHON_PYLINT: true
LINTER_RULES_PATH: '.'
PYTHON_FLAKE8_CONFIG_FILE: .flake8
PYTHON_ISORT_CONFIG_FILE: .isort.cfg
PYTHON_PYLINT_CONFIG_FILE: .pylintrc
2 changes: 2 additions & 0 deletions .isort.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[settings]
skip=tests,venv
26 changes: 26 additions & 0 deletions json_schema_fuzz/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

import exrex

from .merging import merge, merge_list


def random_integer(schema):
"""Generate random integer."""
Expand All @@ -12,8 +14,10 @@ def random_integer(schema):

def random_object(schema):
"""Generate random JSON object."""

properties = schema.get("properties", dict())
required = schema.get("required", [])

object = dict()
for key, value in properties.items():
if key in required or random.choice([True, False]):
Expand Down Expand Up @@ -54,7 +58,29 @@ def random_array(schema):

def generate_json(schema):
"""Generate random JSON conforming to schema."""

# Merge allOf subschemas into the base schema
all_of = schema.get("allOf", [])
for subschema in all_of:
schema = merge(schema, subschema)

any_of = schema.get("anyOf", None)
if any_of:
found = False
while not found:
# Pick a random sample of the any_of sublist
# and determine whether it is valid
chosen_anyof = random.sample(
any_of, random.randint(1, len(any_of)))
try:
combined_anyof_schema = merge_list(chosen_anyof)
found = True
except NotImplementedError:
pass
schema = merge(schema, combined_anyof_schema)

type = schema.get("type", None)

if type == "integer":
return random_integer(schema)
elif type == "object":
Expand Down
50 changes: 50 additions & 0 deletions json_schema_fuzz/merging.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
"""Merging."""
import copy
from typing import Any, Dict, List


def merge(
schema_a: Dict[Any, Any],
schema_b: Dict[Any, Any],
path: List[Any] = None,
in_place: bool = False,
):
"""Merge two JSON schemas recursively."""
if path is None:
path = []

if not in_place:
# Make a copy of a so that it doesn't get modified in place
schema_a = copy.deepcopy(schema_a)

for key in schema_b:
if key not in schema_a:
schema_a[key] = schema_b[key]
continue
if isinstance(schema_a[key], dict) and isinstance(schema_b[key], dict):
merge(schema_a[key], schema_b[key],
path=path + [str(key)],
in_place=True)
elif schema_a[key] == schema_b[key]:
pass # same leaf value
elif (
isinstance(schema_a[key], list)
and isinstance(schema_b[key], list)
):
# Append lists together
schema_a[key].extend(schema_b[key])
else:
raise NotImplementedError(
f"Conflicting key {key} encountered in path {path}")
if in_place:
return None
else:
return schema_a


def merge_list(schemas: List[Dict]) -> Dict:
""" Merge a list of JSON schemas together """
combined_schema = {}
for schema in schemas:
merge(combined_schema, schema, in_place=True)
return combined_schema
7 changes: 7 additions & 0 deletions tests/merge_cases/empties.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"schemas": [
{},
{}
],
"merged": {}
}
31 changes: 31 additions & 0 deletions tests/test_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,21 @@ def test_object():
assert isinstance(output.get("a", 0), int)


def test_allof():
"""Test a schema with an allOf property"""
schema = {
"type": "object",
"allOf": [
{"properties": {"a": {"type": "integer"}}, "required": ["a"]},
{"properties": {"b": {"type": "integer"}}, "required": ["b"]},
],
}
output = generate_json(schema)
assert isinstance(output, dict)
assert isinstance(output["a"], int)
assert isinstance(output["b"], int)


def test_boolean():
"""Test generating booleans."""
schema = {
Expand Down Expand Up @@ -73,3 +88,19 @@ def test_no_pattern_string():
}
output = generate_json(schema)
assert isinstance(output, str)


def test_anyof():
"""Test generating an object from an anyOf schema."""
schema = {
"anyOf": [
{
"type": "string",
},
{
"type": "integer",
},
],
}
output = generate_json(schema)
assert isinstance(output, (str, int))
53 changes: 53 additions & 0 deletions tests/test_merging.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
"""Test JSON schema merging."""
import glob
import json
from pathlib import Path

import pytest

from json_schema_fuzz.merging import merge

THIS_DIR = Path(__file__).parent
CASE_DIR = THIS_DIR / "merge_cases"
case_files = glob.glob(str(CASE_DIR / "*.json"))
cases = []
for filename in case_files:
with open(filename, "r") as stream:
case = json.load(stream)
cases.append((case["schemas"], case["merged"]))


@pytest.mark.parametrize("schemas,merged", cases)
def test_merging(schemas, merged):
"""Test that merging the `schemas` results in the `merged` schema."""
assert merge(*schemas) == merged


def test_merge_doesnt_modify():
""" Test that merging doesn't modify input values """
required_a = ["required_property"]
schema_a = {"required": required_a}
schema_b = {"required": ["another_required_property"]}
merged = merge(schema_a, schema_b)

assert len(merged['required']) == 2
assert len(required_a) == 1


def test_merge_conflicting():
"""Test that merging conflicting values throws a NotImplementedError."""
schema_a = {"multipleOf": 3}
schema_b = {"multipleOf": 5}

with pytest.raises(NotImplementedError):
merge(schema_a, schema_b)


def test_merge_nested():
""" Test that merging nested dictionaries works """
schema_a = {"properties": {"a": "value"}}
schema_b = {"properties": {"b": "value"}}
merged = merge(schema_a, schema_b)

assert 'a' in merged['properties']
assert 'b' in merged['properties']