Skip to content

Commit

Permalink
[json] Add json schema validator
Browse files Browse the repository at this point in the history
  • Loading branch information
fpapon committed Feb 26, 2024
1 parent 15e06fd commit 02484c0
Show file tree
Hide file tree
Showing 30 changed files with 2,740 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* Copyright (c) 2022 - present - Yupiik SAS - https://www.yupiik.com
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package io.yupiik.fusion.json.schema.validation;

import java.util.function.Function;
import java.util.stream.Stream;

import static java.util.Collections.emptyList;
import static java.util.stream.Collectors.toList;

public class JsonSchemaValidator implements Function<Object, ValidationResult>, AutoCloseable {
private static final ValidationResult SUCCESS = new ValidationResult(emptyList());

private final Function<Object, Stream<ValidationResult.ValidationError>> validationFunction;

JsonSchemaValidator(final Function<Object, Stream<ValidationResult.ValidationError>> validationFunction) {
this.validationFunction = validationFunction;
}

@Override
public ValidationResult apply(final Object object) {
final var errors = validationFunction.apply(object).collect(toList());
if (!errors.isEmpty()) {
return new ValidationResult(errors);
}
return SUCCESS;
}

@Override
public void close() {
// no-op
}

@Override
public String toString() {
return "JsonSchemaValidator{validationFunction=" + validationFunction + '}';
}
}

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* Copyright (c) 2022 - present - Yupiik SAS - https://www.yupiik.com
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package io.yupiik.fusion.json.schema.validation;

import java.util.List;

public record ValidationResult(List<ValidationError> errors) {
public boolean isSuccess() {
return errors.isEmpty();
}

public record ValidationError(String field, String message) {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* Copyright (c) 2022 - present - Yupiik SAS - https://www.yupiik.com
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package io.yupiik.fusion.json.schema.validation.spi;

import java.util.Map;
import java.util.function.Function;
import java.util.stream.Stream;

import static java.util.stream.Collectors.joining;

public record ValidationContext(String[] path, Map<String, Object> schema, Function<Object, Object> valueProvider) {
public String toPointer() {
return Stream.of(path).collect(joining("/", "/", ""));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* Copyright (c) 2022 - present - Yupiik SAS - https://www.yupiik.com
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package io.yupiik.fusion.json.schema.validation.spi;

import io.yupiik.fusion.json.schema.validation.ValidationResult;

import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Stream;

public interface ValidationExtension {
Optional<Function<Object, Stream<ValidationResult.ValidationError>>> create(ValidationContext model);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* Copyright (c) 2022 - present - Yupiik SAS - https://www.yupiik.com
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package io.yupiik.fusion.json.schema.validation.spi.builtin;

import io.yupiik.fusion.json.schema.validation.ValidationResult;
import io.yupiik.fusion.json.schema.validation.spi.builtin.type.TypeFilter;

import java.util.function.Function;
import java.util.stream.Stream;

abstract class BaseNumberValidation extends BaseValidation {
protected final double bound;

BaseNumberValidation(final String pointer, final Function<Object, Object> extractor, final double bound) {
super(pointer, extractor, TypeFilter.NUMBER);
this.bound = bound;
}

@Override
protected Stream<ValidationResult.ValidationError> onNumber(final Number number) {
final double val = number.doubleValue();
if (isValid(val)) {
return Stream.empty();
}
return toError(val);
}

protected abstract boolean isValid(double val);

protected abstract Stream<ValidationResult.ValidationError> toError(double val);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/*
* Copyright (c) 2022 - present - Yupiik SAS - https://www.yupiik.com
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package io.yupiik.fusion.json.schema.validation.spi.builtin;

import io.yupiik.fusion.json.schema.validation.ValidationResult;
import io.yupiik.fusion.json.schema.validation.spi.builtin.type.TypeFilter;

import java.util.Collection;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Stream;

public abstract class BaseValidation implements Function<Object, Stream<ValidationResult.ValidationError>> {
protected final String pointer;
protected final Function<Object, Object> extractor;
private final TypeFilter typeValidator;
private final boolean rootCanBeNull;

public BaseValidation(final String pointer, final Function<Object, Object> extractor, final TypeFilter typeValidator) {
this.pointer = pointer;
this.extractor = extractor != null ? extractor : v -> v;
this.rootCanBeNull = extractor != null;
this.typeValidator = typeValidator;
}

@Override
@SuppressWarnings("unchecked")
public Stream<ValidationResult.ValidationError> apply(final Object obj) {
if (obj == null && rootCanBeNull) {
return Stream.empty();
}

final var value = extractor.apply(obj);
if (!typeValidator.test(value)) {
return Stream.empty();
}

if (value instanceof String s) {
return onString(s);
}
if (value instanceof Number n) {
return onNumber(n);
}
if (value instanceof Boolean b) {
return onBoolean(b);
}
if (value instanceof Collection<?> c) {
return onArray(c);
}
if (value instanceof Map<?, ?> m) {
return onObject((Map<String, Object>) m);
}
if (value == null) {
return Stream.empty();
}
throw new IllegalArgumentException("Unsupported value type: " + value);
}

protected Stream<ValidationResult.ValidationError> onArray(final Collection<?> array) {
return Stream.empty();
}

protected Stream<ValidationResult.ValidationError> onObject(final Map<String, Object> object) {
return Stream.empty();
}

protected Stream<ValidationResult.ValidationError> onNumber(final Number number) {
return Stream.empty();
}

protected Stream<ValidationResult.ValidationError> onBoolean(final boolean value) {
return Stream.empty();
}

protected Stream<ValidationResult.ValidationError> onString(final String string) {
return Stream.empty();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
* Copyright (c) 2022 - present - Yupiik SAS - https://www.yupiik.com
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package io.yupiik.fusion.json.schema.validation.spi.builtin;

import io.yupiik.fusion.json.schema.validation.JsonSchemaValidator;
import io.yupiik.fusion.json.schema.validation.JsonSchemaValidatorFactory;
import io.yupiik.fusion.json.schema.validation.ValidationResult;
import io.yupiik.fusion.json.schema.validation.spi.builtin.type.TypeFilter;
import io.yupiik.fusion.json.schema.validation.spi.ValidationContext;
import io.yupiik.fusion.json.schema.validation.spi.ValidationExtension;

import java.util.Collection;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Stream;

public class ContainsValidation implements ValidationExtension {
private final JsonSchemaValidatorFactory factory;

public ContainsValidation(final JsonSchemaValidatorFactory factory) {
this.factory = factory;
}

@Override
@SuppressWarnings("unchecked")
public Optional<Function<Object, Stream<ValidationResult.ValidationError>>> create(final ValidationContext model) {
return Optional.ofNullable(model.schema().get("contains"))
.filter(TypeFilter.OBJECT)
.map(it -> new ItemsValidator(model.toPointer(), model.valueProvider(), factory.newInstance((Map<String, Object>) it)));
}

private static class ItemsValidator extends BaseValidation {
private final JsonSchemaValidator validator;

private ItemsValidator(final String pointer,
final Function<Object, Object> extractor,
JsonSchemaValidator validator) {
super(pointer, extractor, TypeFilter.ARRAY);
this.validator = validator;
}

@Override
protected Stream<ValidationResult.ValidationError> onArray(final Collection<?> array) {
for (final var value : array) {
final var itemErrors = validator.apply(value).errors();
if (itemErrors.isEmpty()) {
return Stream.empty();
}
}
return Stream.of(new ValidationResult.ValidationError(pointer, "No item matching the expected schema"));
}

@Override
public String toString() {
return "Contains{validator=" + validator + ", pointer='" + pointer + "'}";
}
}
}
Loading

0 comments on commit 02484c0

Please sign in to comment.