ghandic / jsf

Creates fake JSON files from a JSON schema
https://ghandic.github.io/jsf
Other
164 stars 36 forks source link

Add support for exclusive minimum and maximum #116

Open nilreml opened 3 months ago

nilreml commented 3 months ago

Somewhat related to #101 - perhaps this helps someone in pinpointing the issue.

Results of running the code below ```text Annotated[int, Gt(gt=0)] Annotated[int, Gt(gt=0), Le(le=1)] Annotated[int, Gt(gt=0), Lt(lt=1)] <- skip: invalid constraints for int Annotated[int, Gt(gt=0), Le(le=2)] Annotated[int, Gt(gt=0), Lt(lt=2)] Annotated[int, Ge(ge=0)] Annotated[int, Ge(ge=0), Le(le=1)] Annotated[int, Ge(ge=0), Lt(lt=1)] Annotated[int, Ge(ge=0), Le(le=2)] Annotated[int, Ge(ge=0), Lt(lt=2)] Annotated[int, Lt(lt=0)] <- fail: empty range for randrange() (0, 0, 0) Annotated[int, Lt(lt=0), Ge(ge=-1)] Annotated[int, Lt(lt=0), Gt(gt=-1)] <- skip: invalid constraints for int Annotated[int, Lt(lt=0), Ge(ge=-2)] Annotated[int, Lt(lt=0), Gt(gt=-2)] Annotated[int, Le(le=0)] Annotated[int, Le(le=0), Ge(ge=-1)] Annotated[int, Le(le=0), Gt(gt=-1)] Annotated[int, Le(le=0), Ge(ge=-2)] Annotated[int, Le(le=0), Gt(gt=-2)] Annotated[float, Gt(gt=0)] Annotated[float, Gt(gt=0), Le(le=1)] Annotated[float, Gt(gt=0), Lt(lt=1)] <- fail: empty range for randrange() (1, 1, 0) Annotated[float, Gt(gt=0), Le(le=2)] Annotated[float, Gt(gt=0), Lt(lt=2)] Annotated[float, Ge(ge=0)] Annotated[float, Ge(ge=0.0), Le(le=1.0)] Annotated[float, Ge(ge=0), Lt(lt=1)] Annotated[float, Ge(ge=0), Le(le=2)] Annotated[float, Ge(ge=0), Lt(lt=2)] Annotated[float, Lt(lt=0)] <- fail: empty range for randrange() (0, 0, 0) Annotated[float, Lt(lt=0), Ge(ge=-1)] Annotated[float, Lt(lt=0), Gt(gt=-1)] <- fail: empty range for randrange() (0, 0, 0) Annotated[float, Lt(lt=0), Ge(ge=-2)] Annotated[float, Lt(lt=0), Gt(gt=-2)] Annotated[float, Le(le=0)] Annotated[float, Le(le=0), Ge(ge=-1)] Annotated[float, Le(le=0), Gt(gt=-1)] Annotated[float, Le(le=0), Ge(ge=-2)] Annotated[float, Le(le=0), Gt(gt=-2)] Annotated[float, Gt(gt=0.1)] Annotated[float, Gt(gt=0.1), Le(le=0.9)] <- fail: empty range for randrange() (2, 1, -1) Annotated[float, Gt(gt=0.1), Lt(lt=0.9)] <- fail: empty range for randrange() (2, 0, -2) Annotated[float, Gt(gt=0.1), Le(le=2.1)] Annotated[float, Gt(gt=0.1), Lt(lt=2.1)] <- fail: empty range for randrange() (2, 2, 0) Annotated[float, Ge(ge=0.1)] Annotated[float, Ge(ge=0.1), Le(le=0.9)] <- fail: empty range for randrange() (1, 1, 0) Annotated[float, Ge(ge=0.1), Lt(lt=0.9)] <- fail: empty range for randrange() (1, 0, -1) Annotated[float, Ge(ge=0.1), Le(le=2.1)] Annotated[float, Ge(ge=0.1), Lt(lt=2.1)] Annotated[float, Lt(lt=-0.1)] <- fail: empty range for randrange() (0, -1, -1) Annotated[float, Lt(lt=-0.1), Ge(ge=-0.9)] <- fail: empty range for randrange() (0, -1, -1) Annotated[float, Lt(lt=-0.1), Gt(gt=-0.9)] <- fail: empty range for randrange() (1, -1, -2) Annotated[float, Lt(lt=-0.1), Ge(ge=-2.1)] Annotated[float, Lt(lt=-0.1), Gt(gt=-2.1)] <- fail: empty range for randrange() (-1, -1, 0) Annotated[float, Le(le=-0.1)] <- fail: empty range for randrange() (0, 0, 0) Annotated[float, Le(le=-0.1), Ge(ge=-0.9)] <- fail: empty range for randrange() (0, 0, 0) Annotated[float, Le(le=-0.1), Gt(gt=-0.9)] <- fail: empty range for randrange() (1, 0, -1) Annotated[float, Le(le=-0.1), Ge(ge=-2.1)] Annotated[float, Le(le=-0.1), Gt(gt=-2.1)] ```
Some json schemata for which generation fails `integer, x < 0`: ```json { "exclusiveMaximum": 0, "type": "integer", "title": "Annotated[int, Lt(lt=0)]: empty range for randrange() (0, 0, 0)" } ``` `number, x < 0`: ```json { "exclusiveMaximum": 0.0, "type": "number", "title": "Annotated[float, Lt(lt=0)]: empty range for randrange() (0, 0, 0)" } ``` `number, 0 < x < 1`: ```json { "exclusiveMaximum": 1.0, "exclusiveMinimum": 0.0, "type": "number", "title": "Annotated[float, Gt(gt=0), Lt(lt=1)]: empty range for randrange() (1, 1, 0)" } ``` `number, 0.1 < x < 0.9`: ```json { "exclusiveMaximum": 0.9, "exclusiveMinimum": 0.1, "type": "number", "title": "Annotated[float, Gt(gt=0.1), Lt(lt=0.9)]: empty range for randrange() (2, 0, -2)" } ``` `number, 0.1 < x <= 0.9`: ```json { "exclusiveMinimum": 0.1, "maximum": 0.9, "type": "number", "title": "Annotated[float, Gt(gt=0.1), Le(le=0.9)]: empty range for randrange() (2, 1, -1)" } ```
Schema generation code (3.11+, pydantic): ```python import json from functools import partial from itertools import chain, product from pathlib import Path from typing import Annotated from annotated_types import Ge, Gt, Le, Lt from jsf import JSF from pydantic import TypeAdapter path = Path("test_schemata").resolve() path.mkdir(parents=True, exist_ok=True) printf = partial(print, end="") cmat_both = [ ([Gt(0), Ge(0)], [None, Le(1), Lt(1), Le(2), Lt(2)]), # Positive, NonNegative ([Lt(0), Le(0)], [None, Ge(-1), Gt(-1), Ge(-2), Gt(-2)]), # Negative, NonPositive ] cmat_float = [ ([Gt(0.1), Ge(0.1)], [None, Le(0.9), Lt(0.9), Le(2.1), Lt(2.1)]), # Positive ([Lt(-0.1), Le(-0.1)], [None, Ge(-0.9), Gt(-0.9), Ge(-2.1), Gt(-2.1)]), # Negative ] invalid_int = [ (Gt(0), Lt(1)), # 0 < x < 1 (Lt(0), Gt(-1)), # -1 < x < 0 ] cons_both = [*chain(*[product(a, b) for a, b in cmat_both])] cons_float = [*chain(*[product(a, b) for a, b in cmat_float])] for origin in [int, float]: for i, c in enumerate(cons_both + cons_float if origin == float else cons_both): # remove None constraints constraints = tuple([x for x in c if x is not None]) typ = Annotated[origin, *constraints] title = str(typ).replace("typing.", "") printf(f"\n{title:<42}") if origin == int and constraints in invalid_int: printf(" <- skip: invalid constraints for int") continue schema = TypeAdapter(typ).json_schema() prefix = "pass" try: JSF(schema).generate() except Exception as e: # noqa: BLE001 printf(f" <- fail: {e}") prefix = "fail" title += f": {e}" # write json schema file schema["title"] = title (path / f"{prefix}_{origin.__name__}_{i:02}.json").write_text(json.dumps(schema, indent=2)) print() ```
ghandic commented 3 months ago

Thanks! PR's welcome, I don't believe we support exclusiveMaximum at the moment hence why tests won't be working.Should be an easy feature for someone to get stuck into