Compare commits

...

4 Commits

Author SHA1 Message Date
torkeln f75f88a623
Merge f6ce123470 into 6cf82328e3 2024-04-23 12:49:33 -04:00
Joel Natividad 6cf82328e3 chore: bump reqwest from 0.11 to 0.12 2024-04-14 21:01:58 +02:00
Dmitry Dygalo 9aae87e573 build: Update builds
Signed-off-by: Dmitry Dygalo <dmitry@dygalo.dev>
2024-04-14 18:24:03 +02:00
Torkel Niklasson f6ce123470 fix: Fix multipleOf to work with decimals
A more simplistic approach to validating floats, where we multiply both
value and multipleOf with 10 until multipleOf does not have any
fractions, and then compare them as integers. This should work since
both numbers are fetched from a text string that can't be repeating.
2023-09-20 16:14:48 +02:00
15 changed files with 276 additions and 285 deletions

View File

@ -10,26 +10,24 @@ jobs:
commitsar:
name: Verify commit messages
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
steps:
- name: Check out code
uses: actions/checkout@v3.0.0
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Run commitsar
uses: aevea/commitsar@v0.18.0
- uses: aevea/commitsar@v0.20.2
pre-commit:
name: Generic pre-commit checks
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
fetch-depth: 1
- uses: actions/setup-python@v4
- uses: actions/setup-python@v5
with:
python-version: 3.9
python-version: 3.11
- run: pip install pre-commit
- run: pre-commit run --all-files
@ -39,30 +37,23 @@ jobs:
strategy:
fail-fast: false
matrix:
os: [ubuntu-20.04, macos-12, windows-2022]
os: [ubuntu-22.04, macos-12, windows-2022]
draft: [draft201909, draft202012]
name: Test ${{ matrix.draft }} (stable) on ${{ matrix.os}}
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
submodules: true
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
- uses: dtolnay/rust-toolchain@stable
- name: Cache cargo
uses: actions/cache@v3
- uses: Swatinem/rust-cache@v2
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: stable-${{ matrix.os }}-${{ matrix.draft }}-cargo-cache
workspaces: jsonschema
cache-all-crates: "true"
key: ${{ matrix.os }}-${{ matrix.draft }}
- run: cargo test --no-fail-fast --features ${{ matrix.draft }}
working-directory: ./jsonschema
@ -74,78 +65,72 @@ jobs:
target: ['wasm32-unknown-unknown']
name: Build on ${{ matrix.target }}
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
submodules: true
- uses: actions-rs/toolchain@v1
- uses: dtolnay/rust-toolchain@stable
with:
profile: minimal
toolchain: stable
target: ${{ matrix.target }}
override: true
targets: ${{ matrix.target }}
- name: Cache cargo
uses: actions/cache@v3
- uses: Swatinem/rust-cache@v2
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: stable-${{ matrix.target }}-cargo-cache
workspaces: jsonschema
cache-all-crates: "true"
- run: cargo build --target ${{ matrix.target }} --no-default-features --features=cli
working-directory: ./jsonschema
coverage:
name: Run test coverage
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
submodules: true
- name: Toolchain setup
uses: actions-rs/toolchain@v1
with:
toolchain: nightly
override: true
- name: Install grcov
run: cargo install cargo-tarpaulin
- name: Cache cargo
uses: actions/cache@v3
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: coverage-cargo-cache
workspaces: jsonschema
cache-all-crates: "true"
- name: Install cargo-llvm-cov
uses: taiki-e/install-action@cargo-llvm-cov
- name: Run tests
run: cargo +nightly tarpaulin --verbose --all-features --out Xml
run: cargo llvm-cov --no-report --all-features
working-directory: ./jsonschema
- name: Generate coverage reports
run: cargo llvm-cov report --lcov --output-path lcov.info
working-directory: ./jsonschema
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
if: ${{ env.GITHUB_REPOSITORY }} == 'Stranger6667/jsonschema-rs'
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
name: coverage
files: lcov.info
fail_ci_if_error: true
test-python:
strategy:
fail-fast: false
matrix:
os: [ubuntu-20.04, macos-12, windows-2022]
os: [ubuntu-22.04, macos-12, windows-2022]
python-version: ['3.7', '3.8', '3.9', '3.10', '3.11', '3.12']
name: Python ${{ matrix.python-version }} on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
submodules: true
- uses: actions/setup-python@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
architecture: x64
@ -153,19 +138,14 @@ jobs:
- run: python -m pip install tox
working-directory: ./bindings/python
- uses: actions-rs/toolchain@v1
with:
toolchain: stable
override: true
- uses: dtolnay/rust-toolchain@stable
- name: Cache cargo
uses: actions/cache@v3
- uses: Swatinem/rust-cache@v2
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: python-${{ matrix.python-version }}-${{ matrix.os }}-cargo-cache
workspaces: |
jsonschema
bindings/python
key: ${{ matrix.python-version }}-${{ matrix.os }}
- name: Run ${{ matrix.python }} tox job
run: tox -e py
@ -173,31 +153,47 @@ jobs:
fmt:
name: Rustfmt
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
- uses: actions-rs/toolchain@v1
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@nightly
with:
profile: minimal
toolchain: stable
override: true
components: rustfmt
- run: cargo fmt --all -- --check
working-directory: ./jsonschema
- run: cargo fmt --all -- --check
working-directory: ./bindings/python
- run: cargo fmt --all -- --check
working-directory: ./bench_helpers
- run: cargo fmt --all -- --check
working-directory: ./perf-helpers
- run: cargo fmt --all -- --check
working-directory: ./jsonschema-test-suite
clippy:
name: Clippy
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
submodules: true
- uses: actions-rs/toolchain@v1
- uses: dtolnay/rust-toolchain@stable
with:
profile: minimal
toolchain: stable
override: true
components: clippy
- uses: Swatinem/rust-cache@v2
with:
workspaces: |
jsonschema
bindings/python
- run: cargo clippy --all-targets --all-features -- -D warnings
working-directory: ./jsonschema
@ -206,16 +202,20 @@ jobs:
features:
name: Check features
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
submodules: true
- uses: actions-rs/toolchain@v1
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
with:
profile: minimal
toolchain: stable
override: true
workspaces: jsonschema
cache-all-crates: "true"
- uses: taiki-e/install-action@cargo-hack
- run: cargo hack check --feature-powerset --lib
working-directory: ./jsonschema

View File

@ -1,9 +1,9 @@
default_language_version:
python: python3.9
python: python3.11
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.1.0
rev: v4.6.0
hooks:
- id: check-yaml
- id: end-of-file-fixer
@ -15,23 +15,21 @@ repos:
- id: check-merge-conflict
- repo: https://github.com/jorisroovers/gitlint
rev: v0.17.0
rev: v0.19.1
hooks:
- id: gitlint
- repo: https://github.com/adrienverge/yamllint
rev: v1.26.3
rev: v1.35.1
hooks:
- id: yamllint
- repo: https://github.com/ambv/black
rev: 22.3.0
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.3.7
hooks:
- id: black
types: [python]
- id: ruff-format
- repo: https://github.com/pre-commit/mirrors-isort
rev: v5.10.1
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.3.7
hooks:
- id: isort
additional_dependencies: ["isort[pyproject]"]
- id: ruff

View File

@ -13,6 +13,7 @@
- Bump `percent-encoding` to `2.3`.
- Bump `regex` to `1.10`.
- Bump `url` to `2.5`.
- Build CLI only if the `cli` feature is enabled.
## [0.17.1] - 2023-07-05

View File

@ -0,0 +1,2 @@
imports_granularity = "Crate"
edition = "2021"

View File

@ -36,8 +36,22 @@ CANADA = load_from_benches("canada.json")
CITM_CATALOG_SCHEMA = load_from_benches("citm_catalog_schema.json")
CITM_CATALOG = load_from_benches("citm_catalog.json")
FAST_SCHEMA = load_from_benches("fast_schema.json")
FAST_INSTANCE_VALID = [9, "hello", [1, "a", True], {"a": "a", "b": "b", "d": "d"}, 42, 3]
FAST_INSTANCE_INVALID = [10, "world", [1, "a", True], {"a": "a", "b": "b", "c": "xy"}, "str", 5]
FAST_INSTANCE_VALID = [
9,
"hello",
[1, "a", True],
{"a": "a", "b": "b", "d": "d"},
42,
3,
]
FAST_INSTANCE_INVALID = [
10,
"world",
[1, "a", True],
{"a": "a", "b": "b", "c": "xy"},
"str",
5,
]
@pytest.fixture(params=[True, False], ids=("compiled", "raw"))
@ -46,7 +60,12 @@ def is_compiled(request):
if jsonschema_rs is not None:
variants = ["jsonschema-rs-is-valid", "jsonschema-rs-validate", "jsonschema", "fastjsonschema"]
variants = [
"jsonschema-rs-is-valid",
"jsonschema-rs-validate",
"jsonschema",
"fastjsonschema",
]
else:
variants = ["jsonschema", "fastjsonschema"]
@ -66,12 +85,20 @@ def args(request, variant, is_compiled):
if is_compiled:
return jsonschema_rs.JSONSchema(schema, with_meta_schemas=True).is_valid, instance
else:
return partial(jsonschema_rs.is_valid, with_meta_schemas=True), schema, instance
return (
partial(jsonschema_rs.is_valid, with_meta_schemas=True),
schema,
instance,
)
if variant == "jsonschema-rs-validate":
if is_compiled:
return jsonschema_rs.JSONSchema(schema, with_meta_schemas=True).validate, instance
else:
return partial(jsonschema_rs.validate, with_meta_schemas=True), schema, instance
return (
partial(jsonschema_rs.validate, with_meta_schemas=True),
schema,
instance,
)
if variant == "jsonschema":
if is_compiled:
return jsonschema.validators.validator_for(schema)(schema).is_valid, instance
@ -85,7 +112,14 @@ def args(request, variant, is_compiled):
@pytest.mark.parametrize(
"name", ("openapi.json", "swagger.json", "geojson.json", "citm_catalog_schema.json", "fast_schema.json")
"name",
(
"openapi.json",
"swagger.json",
"geojson.json",
"citm_catalog_schema.json",
"fast_schema.json",
),
)
@pytest.mark.parametrize(
"func",

View File

@ -36,18 +36,16 @@ Changelog = "https://github.com/Stranger6667/jsonschema-rs/blob/master/bindings/
Source = "https://github.com/Stranger6667/jsonschema-rs"
Funding = 'https://github.com/sponsors/Stranger6667'
[tool.black]
[tool.ruff]
line-length = 120
target_version = ["py37"]
target-version = "py37"
[tool.isort]
# config compatible with Black
line_length = 120
multi_line_output = 3
default_section = "THIRDPARTY"
include_trailing_comma = true
known_first_party = "jsonschema_rs"
known_third_party = []
[tool.ruff.lint.isort]
known-first-party = ["jsonschema_rs"]
known-third-party = ["hypothesis", "pytest"]
[tool.ruff.format]
skip-magic-trailing-comma = false
[tool.maturin]
python-source = "python"

View File

@ -1 +1 @@
from .jsonschema_rs import *
from .jsonschema_rs import * # noqa: F403

View File

@ -1,51 +1,48 @@
from typing import Any, TypeVar
from collections.abc import Iterator
_SchemaT = TypeVar('_SchemaT', bool, dict[str, Any])
_SchemaT = TypeVar("_SchemaT", bool, dict[str, Any])
def is_valid(
schema: _SchemaT,
instance: Any,
draft: int | None = None,
with_meta_schemas: bool | None = None
schema: _SchemaT,
instance: Any,
draft: int | None = None,
with_meta_schemas: bool | None = None,
) -> bool:
pass
def validate(
schema: _SchemaT,
instance: Any,
draft: int | None = None,
with_meta_schemas: bool | None = None
schema: _SchemaT,
instance: Any,
draft: int | None = None,
with_meta_schemas: bool | None = None,
) -> None:
pass
def iter_errors(
schema: _SchemaT,
instance: Any,
draft: int | None = None,
with_meta_schemas: bool | None = None
schema: _SchemaT,
instance: Any,
draft: int | None = None,
with_meta_schemas: bool | None = None,
) -> Iterator[ValidationError]:
pass
class JSONSchema:
def __init__(
self,
schema: _SchemaT,
draft: int | None = None,
with_meta_schemas: bool | None = None
self,
schema: _SchemaT,
draft: int | None = None,
with_meta_schemas: bool | None = None,
) -> None:
pass
@classmethod
def from_str(
cls,
schema: str,
draft: int | None = None,
with_meta_schemas: bool | None = None
) -> 'JSONSchema':
cls,
schema: str,
draft: int | None = None,
with_meta_schemas: bool | None = None,
) -> "JSONSchema":
pass
def is_valid(self, instance: Any) -> bool:
@ -57,13 +54,11 @@ class JSONSchema:
def iter_errors(self, instance: Any) -> Iterator[ValidationError]:
pass
class ValidationError(ValueError):
message: str
schema_path: list[str | int]
instance_path: list[str | int]
Draft4: int
Draft6: int
Draft7: int

View File

@ -0,0 +1,2 @@
imports_granularity = "Crate"
edition = "2021"

View File

@ -0,0 +1,2 @@
imports_granularity = "Crate"
edition = "2021"

View File

@ -53,7 +53,7 @@ once_cell = "1.19"
parking_lot = "0.12"
percent-encoding = "2.3"
regex = "1.10"
reqwest = { version = "0.11", features = [
reqwest = { version = "0.12", features = [
"blocking",
"json",
], default-features = false, optional = true }

2
jsonschema/rustfmt.toml Normal file
View File

@ -0,0 +1,2 @@
imports_granularity = "Crate"
edition = "2021"

View File

@ -9,37 +9,33 @@ use crate::{
use fraction::{BigFraction, BigUint};
use serde_json::{Map, Value};
pub(crate) struct MultipleOfFloatValidator {
pub(crate) struct MultipleOfValidator {
multiple_of: f64,
schema_path: JSONPointer,
}
impl MultipleOfFloatValidator {
impl MultipleOfValidator {
#[inline]
pub(crate) fn compile<'a>(multiple_of: f64, schema_path: JSONPointer) -> CompilationResult<'a> {
Ok(Box::new(MultipleOfFloatValidator {
Ok(Box::new(MultipleOfValidator {
multiple_of,
schema_path,
}))
}
}
impl Validate for MultipleOfFloatValidator {
impl Validate for MultipleOfValidator {
fn is_valid(&self, instance: &Value) -> bool {
if let Value::Number(item) = instance {
let item = item.as_f64().expect("Always valid");
let remainder = (item / self.multiple_of) % 1.;
if remainder.is_nan() {
// Involves heap allocations via the underlying `BigUint` type
let fraction = BigFraction::from(item) / BigFraction::from(self.multiple_of);
if let Some(denom) = fraction.denom() {
denom == &BigUint::from(1_u8)
} else {
true
}
} else {
remainder < f64::EPSILON
let mut tmp_item = item.as_f64().expect("Always valid");
let mut tmp_multiple_of = self.multiple_of;
while tmp_item.fract() != 0. {
tmp_item *= 10.0;
tmp_multiple_of *= 10.0;
}
tmp_item % tmp_multiple_of == 0.0
} else {
true
}
@ -62,57 +58,7 @@ impl Validate for MultipleOfFloatValidator {
}
}
impl core::fmt::Display for MultipleOfFloatValidator {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "multipleOf: {}", self.multiple_of)
}
}
pub(crate) struct MultipleOfIntegerValidator {
multiple_of: f64,
schema_path: JSONPointer,
}
impl MultipleOfIntegerValidator {
#[inline]
pub(crate) fn compile<'a>(multiple_of: f64, schema_path: JSONPointer) -> CompilationResult<'a> {
Ok(Box::new(MultipleOfIntegerValidator {
multiple_of,
schema_path,
}))
}
}
impl Validate for MultipleOfIntegerValidator {
fn is_valid(&self, instance: &Value) -> bool {
if let Value::Number(item) = instance {
let item = item.as_f64().expect("Always valid");
// As the divisor has its fractional part as zero, then any value with a non-zero
// fractional part can't be a multiple of this divisor, therefore it is short-circuited
item.fract() == 0. && (item % self.multiple_of) == 0.
} else {
true
}
}
fn validate<'instance>(
&self,
instance: &'instance Value,
instance_path: &InstancePath,
) -> ErrorIterator<'instance> {
if !self.is_valid(instance) {
return error(ValidationError::multiple_of(
self.schema_path.clone(),
instance_path.into(),
instance,
self.multiple_of,
));
}
no_error()
}
}
impl core::fmt::Display for MultipleOfIntegerValidator {
impl core::fmt::Display for MultipleOfValidator {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "multipleOf: {}", self.multiple_of)
}
@ -126,14 +72,10 @@ pub(crate) fn compile<'a>(
if let Value::Number(multiple_of) = schema {
let multiple_of = multiple_of.as_f64().expect("Always valid");
let schema_path = context.as_pointer_with("multipleOf");
if multiple_of.fract() == 0. {
Some(MultipleOfIntegerValidator::compile(
multiple_of,
schema_path,
))
} else {
Some(MultipleOfFloatValidator::compile(multiple_of, schema_path))
}
Some(MultipleOfValidator::compile(
multiple_of,
schema_path,
))
} else {
Some(Err(ValidationError::single_type_error(
JSONPointer::default(),
@ -154,11 +96,18 @@ mod tests {
#[test_case(&json!({"multipleOf": 1.0}), &json!(4.0))]
#[test_case(&json!({"multipleOf": 1.5}), &json!(3.0))]
#[test_case(&json!({"multipleOf": 1.5}), &json!(4.5))]
#[test_case(&json!({"multipleOf": 0.1}), &json!(1.1))]
#[test_case(&json!({"multipleOf": 0.1}), &json!(1.2))]
#[test_case(&json!({"multipleOf": 0.1}), &json!(1.3))]
#[test_case(&json!({"multipleOf": 0.02}), &json!(1.02))]
fn multiple_of_is_valid(schema: &Value, instance: &Value) {
tests_util::is_valid(schema, instance)
}
#[test_case(&json!({"multipleOf": 1.0}), &json!(4.5))]
#[test_case(&json!({"multipleOf": 0.1}), &json!(4.55))]
#[test_case(&json!({"multipleOf": 0.2}), &json!(4.5))]
#[test_case(&json!({"multipleOf": 0.02}), &json!(1.01))]
fn multiple_of_is_not_valid(schema: &Value, instance: &Value) {
tests_util::is_not_valid(schema, instance)
}

View File

@ -1,33 +1,76 @@
use std::{
error::Error,
fs::File,
io::BufReader,
path::{Path, PathBuf},
process,
};
use clap::Parser;
use jsonschema::JSONSchema;
type BoxErrorResult<T> = Result<T, Box<dyn Error>>;
#[derive(Parser)]
#[command(name = "jsonschema")]
struct Cli {
/// A path to a JSON instance (i.e. filename.json) to validate (may be specified multiple times).
#[arg(short = 'i', long = "instance")]
instances: Option<Vec<PathBuf>>,
/// The JSON Schema to validate with (i.e. schema.json).
#[arg(value_parser, required_unless_present("version"))]
schema: Option<PathBuf>,
/// Show program's version number and exit.
#[arg(short = 'v', long = "version")]
version: bool,
#[cfg(not(feature = "cli"))]
fn main() {
eprintln!("`jsonschema` CLI is only available with the `cli` feature");
std::process::exit(1);
}
pub fn main() -> BoxErrorResult<()> {
#[cfg(feature = "cli")]
fn main() -> Result<(), Box<dyn std::error::Error>> {
use std::{
fs::File,
io::BufReader,
path::{Path, PathBuf},
process,
};
use clap::Parser;
use jsonschema::JSONSchema;
#[derive(Parser)]
#[command(name = "jsonschema")]
struct Cli {
/// A path to a JSON instance (i.e. filename.json) to validate (may be specified multiple times).
#[arg(short = 'i', long = "instance")]
instances: Option<Vec<PathBuf>>,
/// The JSON Schema to validate with (i.e. schema.json).
#[arg(value_parser, required_unless_present("version"))]
schema: Option<PathBuf>,
/// Show program's version number and exit.
#[arg(short = 'v', long = "version")]
version: bool,
}
fn read_json(path: &Path) -> serde_json::Result<serde_json::Value> {
let file = File::open(path).expect("Failed to open file");
let reader = BufReader::new(file);
serde_json::from_reader(reader)
}
fn validate_instances(
instances: &[PathBuf],
schema_path: PathBuf,
) -> Result<bool, Box<dyn std::error::Error>> {
let mut success = true;
let schema_json = read_json(&schema_path)?;
match JSONSchema::compile(&schema_json) {
Ok(schema) => {
for instance in instances {
let instance_json = read_json(instance)?;
let validation = schema.validate(&instance_json);
let filename = instance.to_string_lossy();
match validation {
Ok(_) => println!("{} - VALID", filename),
Err(errors) => {
success = false;
println!("{} - INVALID. Errors:", filename);
for (i, e) in errors.enumerate() {
println!("{}. {}", i + 1, e);
}
}
}
}
}
Err(error) => {
println!("Schema is invalid. Error: {}", error);
success = false;
}
}
Ok(success)
}
let config = Cli::parse();
if config.version {
@ -48,40 +91,3 @@ pub fn main() -> BoxErrorResult<()> {
Ok(())
}
fn read_json(path: &Path) -> serde_json::Result<serde_json::Value> {
let file = File::open(path).expect("Failed to open file");
let reader = BufReader::new(file);
serde_json::from_reader(reader)
}
fn validate_instances(instances: &[PathBuf], schema_path: PathBuf) -> BoxErrorResult<bool> {
let mut success = true;
let schema_json = read_json(&schema_path)?;
match JSONSchema::compile(&schema_json) {
Ok(schema) => {
for instance in instances {
let instance_json = read_json(instance)?;
let validation = schema.validate(&instance_json);
let filename = instance.to_string_lossy();
match validation {
Ok(_) => println!("{} - VALID", filename),
Err(errors) => {
success = false;
println!("{} - INVALID. Errors:", filename);
for (i, e) in errors.enumerate() {
println!("{}. {}", i + 1, e);
}
}
}
}
}
Err(error) => {
println!("Schema is invalid. Error: {}", error);
success = false;
}
}
Ok(success)
}

View File

@ -0,0 +1,2 @@
imports_granularity = "Crate"
edition = "2021"