001package gudusoft.gsqlparser.slpc.validation;
002
003import gudusoft.gsqlparser.slpc.model.SlpcObject;
004
005import java.math.BigInteger;
006import java.util.ArrayList;
007import java.util.HashSet;
008import java.util.List;
009import java.util.Set;
010import java.util.regex.Pattern;
011
012/**
013 * Dependency-free Draft-07 evaluator for the keyword vocabulary used by the
014 * checked-in SLPC v0.2 schema.  The schema remains the source of truth: this
015 * class reads it at runtime rather than reproducing its fields in Java.
016 */
017public final class SlpcSchemaValidator {
018
019    private final SlpcObject rootSchema;
020
021    public SlpcSchemaValidator(SlpcObject rootSchema) {
022        if (rootSchema == null) throw new IllegalArgumentException("schema must not be null");
023        this.rootSchema = rootSchema;
024    }
025
026    public List<SlpcValidationError> validate(Object instance) {
027        ArrayList<SlpcValidationError> errors = new ArrayList<SlpcValidationError>();
028        evaluate(rootSchema, instance, "$", errors);
029        return errors;
030    }
031
032    public List<SlpcValidationError> validateDefinition(String definitionName, Object instance) {
033        SlpcObject definitions = rootSchema.object("definitions");
034        if (definitions == null || !definitions.contains(definitionName)) {
035            throw new IllegalArgumentException("missing JSON Schema definition: " + definitionName);
036        }
037        ArrayList<SlpcValidationError> errors = new ArrayList<SlpcValidationError>();
038        evaluate(definitions.get(definitionName), instance, "$", errors);
039        return errors;
040    }
041
042    private void evaluate(Object schemaValue, Object instance, String path, List<SlpcValidationError> errors) {
043        if (Boolean.TRUE.equals(schemaValue)) return;
044        if (Boolean.FALSE.equals(schemaValue)) {
045            error(errors, path, "value is rejected by boolean schema");
046            return;
047        }
048        if (!(schemaValue instanceof SlpcObject)) {
049            throw new IllegalArgumentException("invalid JSON Schema node at " + path);
050        }
051        SlpcObject schema = (SlpcObject) schemaValue;
052
053        String ref = schema.string("$ref");
054        if (ref != null) {
055            evaluate(resolve(ref), instance, path, errors);
056            return;
057        }
058
059        if (schema.contains("type") && !matchesType(schema.get("type"), instance)) {
060            error(errors, path, printable(instance) + " is not of type " + printable(schema.get("type")));
061            return;
062        }
063        if (schema.contains("const") && !jsonEquals(schema.get("const"), instance)) {
064            error(errors, path, printable(instance) + " is not equal to const " + printable(schema.get("const")));
065        }
066        List<Object> enumValues = schema.array("enum");
067        if (enumValues != null && !containsJson(enumValues, instance)) {
068            error(errors, path, printable(instance) + " is not one of " + printable(enumValues));
069        }
070
071        evaluateCombinators(schema, instance, path, errors);
072
073        if (instance instanceof SlpcObject) evaluateObject(schema, (SlpcObject) instance, path, errors);
074        if (instance instanceof List<?>) evaluateArray(schema, (List<?>) instance, path, errors);
075        if (instance instanceof String) evaluateString(schema, (String) instance, path, errors);
076        if (instance instanceof BigInteger) evaluateInteger(schema, (BigInteger) instance, path, errors);
077    }
078
079    private void evaluateCombinators(SlpcObject schema, Object instance, String path, List<SlpcValidationError> errors) {
080        List<Object> allOf = schema.array("allOf");
081        if (allOf != null) for (Object branch : allOf) evaluate(branch, instance, path, errors);
082
083        List<Object> anyOf = schema.array("anyOf");
084        if (anyOf != null) {
085            boolean accepted = false;
086            for (Object branch : anyOf) if (valid(branch, instance, path)) { accepted = true; break; }
087            if (!accepted) error(errors, path, printable(instance) + " is not valid under any of the given schemas");
088        }
089
090        List<Object> oneOf = schema.array("oneOf");
091        if (oneOf != null) {
092            int accepted = 0;
093            for (Object branch : oneOf) if (valid(branch, instance, path)) accepted++;
094            if (accepted != 1) error(errors, path, printable(instance) + " is valid under " + accepted + " oneOf schemas");
095        }
096
097        if (schema.contains("not") && valid(schema.get("not"), instance, path)) {
098            error(errors, path, printable(instance) + " must not validate against the prohibited schema");
099        }
100
101        if (schema.contains("if")) {
102            if (valid(schema.get("if"), instance, path)) {
103                if (schema.contains("then")) evaluate(schema.get("then"), instance, path, errors);
104            } else if (schema.contains("else")) {
105                evaluate(schema.get("else"), instance, path, errors);
106            }
107        }
108    }
109
110    private void evaluateObject(SlpcObject schema, SlpcObject instance, String path, List<SlpcValidationError> errors) {
111        List<Object> required = schema.array("required");
112        if (required != null) {
113            for (Object name : required) {
114                if (!instance.contains((String) name)) error(errors, path, "'" + name + "' is a required property");
115            }
116        }
117        SlpcObject properties = schema.object("properties");
118        Set<String> known = new HashSet<String>();
119        if (properties != null) {
120            known.addAll(properties.names());
121            for (String name : properties.names()) {
122                if (instance.contains(name)) evaluate(properties.get(name), instance.get(name), child(path, name), errors);
123            }
124        }
125        if (schema.contains("additionalProperties")) {
126            Object additional = schema.get("additionalProperties");
127            List<String> unexpected = new ArrayList<String>();
128            for (String name : instance.names()) {
129                if (known.contains(name)) continue;
130                if (Boolean.FALSE.equals(additional)) unexpected.add(name);
131                else if (!Boolean.TRUE.equals(additional)) evaluate(additional, instance.get(name), child(path, name), errors);
132            }
133            if (!unexpected.isEmpty()) error(errors, path, "Additional properties are not allowed: " + unexpected);
134        }
135    }
136
137    private void evaluateArray(SlpcObject schema, List<?> instance, String path, List<SlpcValidationError> errors) {
138        BigInteger minimum = schema.integer("minItems");
139        BigInteger maximum = schema.integer("maxItems");
140        if (minimum != null && instance.size() < minimum.intValue()) error(errors, path, instance + " is too short");
141        if (maximum != null && instance.size() > maximum.intValue()) error(errors, path, instance + " is too long");
142        if (Boolean.TRUE.equals(schema.get("uniqueItems"))) {
143            for (int left = 0; left < instance.size(); left++) {
144                for (int right = left + 1; right < instance.size(); right++) {
145                    if (jsonEquals(instance.get(left), instance.get(right))) {
146                        error(errors, path, "array has non-unique elements at " + left + " and " + right);
147                    }
148                }
149            }
150        }
151        if (schema.contains("items")) {
152            Object itemSchema = schema.get("items");
153            if (itemSchema instanceof List<?>) {
154                List<?> tuple = (List<?>) itemSchema;
155                for (int index = 0; index < instance.size() && index < tuple.size(); index++) {
156                    evaluate(tuple.get(index), instance.get(index), path + "[" + index + "]", errors);
157                }
158            } else {
159                for (int index = 0; index < instance.size(); index++) {
160                    evaluate(itemSchema, instance.get(index), path + "[" + index + "]", errors);
161                }
162            }
163        }
164        if (schema.contains("contains")) {
165            boolean found = false;
166            for (int index = 0; index < instance.size(); index++) {
167                if (valid(schema.get("contains"), instance.get(index), path + "[" + index + "]")) {
168                    found = true;
169                    break;
170                }
171            }
172            if (!found) error(errors, path, "array does not contain an item matching the required schema");
173        }
174    }
175
176    private void evaluateString(SlpcObject schema, String instance, String path, List<SlpcValidationError> errors) {
177        BigInteger minimum = schema.integer("minLength");
178        BigInteger maximum = schema.integer("maxLength");
179        int length = instance.codePointCount(0, instance.length());
180        if (minimum != null && length < minimum.intValue()) error(errors, path, "'" + instance + "' is too short");
181        if (maximum != null && length > maximum.intValue()) error(errors, path, "'" + instance + "' is too long");
182        String pattern = schema.string("pattern");
183        if (pattern != null && !Pattern.compile(pattern).matcher(instance).find()) {
184            error(errors, path, "'" + instance + "' does not match '" + pattern + "'");
185        }
186    }
187
188    private void evaluateInteger(SlpcObject schema, BigInteger instance, String path, List<SlpcValidationError> errors) {
189        BigInteger minimum = schema.integer("minimum");
190        BigInteger maximum = schema.integer("maximum");
191        if (minimum != null && instance.compareTo(minimum) < 0) error(errors, path, instance + " is less than the minimum of " + minimum);
192        if (maximum != null && instance.compareTo(maximum) > 0) error(errors, path, instance + " is greater than the maximum of " + maximum);
193    }
194
195    private boolean valid(Object schema, Object instance, String path) {
196        ArrayList<SlpcValidationError> trial = new ArrayList<SlpcValidationError>();
197        evaluate(schema, instance, path, trial);
198        return trial.isEmpty();
199    }
200
201    private Object resolve(String ref) {
202        if (!ref.startsWith("#/")) throw new IllegalArgumentException("only local JSON Schema references are supported: " + ref);
203        Object current = rootSchema;
204        String[] segments = ref.substring(2).split("/");
205        for (String encoded : segments) {
206            String segment = encoded.replace("~1", "/").replace("~0", "~");
207            if (!(current instanceof SlpcObject) || !((SlpcObject) current).contains(segment)) {
208                throw new IllegalArgumentException("unresolved JSON Schema reference: " + ref);
209            }
210            current = ((SlpcObject) current).get(segment);
211        }
212        return current;
213    }
214
215    private boolean matchesType(Object declared, Object instance) {
216        if (declared instanceof String) return matchesTypeName((String) declared, instance);
217        if (declared instanceof List<?>) {
218            for (Object type : (List<?>) declared) if (matchesTypeName((String) type, instance)) return true;
219            return false;
220        }
221        throw new IllegalArgumentException("invalid type keyword");
222    }
223
224    private boolean matchesTypeName(String type, Object instance) {
225        if ("null".equals(type)) return instance == null;
226        if ("object".equals(type)) return instance instanceof SlpcObject;
227        if ("array".equals(type)) return instance instanceof List<?>;
228        if ("string".equals(type)) return instance instanceof String;
229        if ("boolean".equals(type)) return instance instanceof Boolean;
230        if ("integer".equals(type) || "number".equals(type)) return instance instanceof BigInteger;
231        throw new IllegalArgumentException("unsupported JSON Schema type: " + type);
232    }
233
234    private static boolean containsJson(List<Object> values, Object target) {
235        for (Object value : values) if (jsonEquals(value, target)) return true;
236        return false;
237    }
238
239    private static boolean jsonEquals(Object left, Object right) {
240        return left == null ? right == null : left.equals(right);
241    }
242
243    private static String printable(Object value) {
244        if (value == null) return "null";
245        if (value instanceof String) return "'" + value + "'";
246        return String.valueOf(value);
247    }
248
249    private static String child(String path, String member) {
250        return member.matches("[A-Za-z_][A-Za-z0-9_]*") ? path + "." + member : path + "['" + member + "']";
251    }
252
253    private static void error(List<SlpcValidationError> errors, String path, String message) {
254        errors.add(new SlpcValidationError(SlpcValidationError.Stage.SCHEMA, path, message));
255    }
256}