Skip to content

Commit

Permalink
Added additionalProperties, patternProperties, properties and required
Browse files Browse the repository at this point in the history
The additionalProperties test file is not included as these tests use
"allOf" which is not implemented

Moved external dependencies to the libs folder
  • Loading branch information
DrDeano committed Nov 27, 2022
1 parent ba46be7 commit c6769f2
Show file tree
Hide file tree
Showing 7 changed files with 214 additions and 16 deletions.
9 changes: 6 additions & 3 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
[submodule "JSON-Schema-Test-Suite"]
path = JSON-Schema-Test-Suite
url = https://github.com/json-schema-org/JSON-Schema-Test-Suite.git
[submodule "libs/JSON-Schema-Test-Suite"]
path = libs/JSON-Schema-Test-Suite
url = https://github.com/json-schema-org/JSON-Schema-Test-Suite.git
[submodule "libs/zig-regex"]
path = libs/zig-regex
url = https://github.com/DrDeano/zig-regex.git
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ A Zig implementation of the JSON schema validator.

## Supported Validations (Draft 7)

- [ ] additionalProperties
- [x] additionalProperties
- [ ] allOf
- [ ] anyOf
- [x] boolean_schema
Expand Down Expand Up @@ -44,12 +44,12 @@ A Zig implementation of the JSON schema validator.
- [ ] oneOf
- [ ] opt
- [ ] pattern
- [ ] patternProperties
- [ ] properties
- [x] patternProperties
- [x] properties
- [ ] propertyNames
- [ ] ref
- [ ] refRemote
- [ ] required
- [x] required
- [x] type
- [ ] uniqueItems
- [ ] unknownKeyword
Expand Down
4 changes: 4 additions & 0 deletions build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,22 @@ pub fn build(b: *std.build.Builder) void {
// between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall.
const mode = b.standardReleaseOptions();

const regex_pkg = std.build.Pkg{ .name = "zig-regex", .source = .{ .path = "libs/zig-regex/src/regex.zig" } };

const lib = b.addStaticLibrary("jsonschema", "src/jsonschema.zig");
lib.setBuildMode(mode);
lib.install();

const c_lib = b.addStaticLibrary("c_jsonschema", "src/c_jsonschema.zig");
c_lib.setBuildMode(mode);
c_lib.addPackage(regex_pkg);
c_lib.linkLibC();
c_lib.step.dependOn(&lib.step);
c_lib.install();

const jsonschema_tests = b.addTest("src/tests.zig");
jsonschema_tests.setBuildMode(mode);
jsonschema_tests.addPackage(regex_pkg);
jsonschema_tests.linkLibrary(c_lib);

const test_step = b.step("test", "Run library tests");
Expand Down
1 change: 1 addition & 0 deletions libs/zig-regex
Submodule zig-regex added at d73caa
197 changes: 192 additions & 5 deletions src/jsonschema.zig
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ const std = @import("std");
const testing = std.testing;
const Allocator = std.mem.Allocator;

const regex = @import("zig-regex");

const Type = enum {
Object,
Array,
Expand Down Expand Up @@ -209,13 +211,141 @@ const MinimumMaximum = struct {
}
};

const Pattern = struct {
pattern: []const u8,
required: bool,
};

const AllPattern = struct {
pattern: union(enum) {
All: Pattern,
Regex: regex.Regex,
},
matches: Schema,
};

const PatternMatch = struct {
pattern: std.ArrayList(AllPattern),
// Would prefer ?Schema but complier broken
// For now this is a pointer and will need to be freed
not_matches: ?*Schema,
required_count: i64,

const Self = @This();

pub fn compile(allocator: Allocator, properties: ?std.json.Value, pattern_properties: ?std.json.Value, additional_properties: ?std.json.Value, required: ?std.json.Value) Schema.CompileError!Self {
var patterns = std.ArrayList(AllPattern).init(allocator);

var required_count: i64 = 0;
if (required) |req| {
for (req.Array.items) |elem| {
try patterns.append(.{
.pattern = .{ .All = .{ .pattern = elem.String, .required = true } },
.matches = Schema{ .Bool = true },
});
}
required_count = @intCast(i64, req.Array.items.len);
}

if (properties) |prop| {
var prop_it = prop.Object.iterator();
while (prop_it.next()) |elem| {
for (patterns.items) |*elem2| {
if (std.mem.eql(u8, elem.key_ptr.*, elem2.pattern.All.pattern)) {
elem2.matches = try Schema.compile(allocator, elem.value_ptr.*);
}
} else {
try patterns.append(.{
.pattern = .{ .All = .{ .pattern = elem.key_ptr.*, .required = false } },
.matches = try Schema.compile(allocator, elem.value_ptr.*),
});
}
}
}

if (pattern_properties) |prop| {
var prop_it = prop.Object.iterator();
while (prop_it.next()) |elem| {
try patterns.append(.{
.pattern = .{ .Regex = try regex.Regex.compile(allocator, elem.key_ptr.*) },
.matches = try Schema.compile(allocator, elem.value_ptr.*),
});
}
}

var not_matches: ?*Schema = null;
if (additional_properties) |add_prop| {
not_matches = try allocator.create(Schema);
not_matches.?.* = try Schema.compile(allocator, add_prop);
}

return .{
.pattern = patterns,
.not_matches = not_matches,
.required_count = required_count,
};
}

pub fn validate(self: Self, data: std.json.Value) Schema.ValidateError!bool {
switch (data) {
.Object => |obj| {
var required_matches: usize = 0;
var obj_it = obj.iterator();
while (obj_it.next()) |obj_val| {
var failed_match = false;
var has_match = false;
for (self.pattern.items) |*pattern| {
switch (pattern.pattern) {
.All => |pat| {
if (std.mem.eql(u8, pat.pattern, obj_val.key_ptr.*)) {
has_match = true;
if (pat.required) {
required_matches += 1;
}
if (!try pattern.matches.validate(obj_val.value_ptr.*)) {
failed_match = true;
break;
}
}
},
.Regex => |*re| {
if (try re.partialMatch(obj_val.key_ptr.*)) {
has_match = true;
if (!try pattern.matches.validate(obj_val.value_ptr.*)) {
failed_match = true;
break;
}
}
},
}
}
if (!has_match or failed_match) {
if (self.not_matches) |not_matches| {
if (!try not_matches.validate(obj_val.value_ptr.*)) {
return false;
}
}
// Move this above the first if (test speed)
if (failed_match) {
return false;
}
}
}
return self.required_count <= required_matches;
},
else => return true,
}
}
};

/// The root compiled schema object
pub const Schema = union(enum) {
Schemas: []Schema,
Bool: bool,
Types: Types,
MinMaxItems: MinMaxItems,
MinimumMaximum: MinimumMaximum,
PatternMatch: PatternMatch,

const Self = @This();

Expand All @@ -229,13 +359,13 @@ pub const Schema = union(enum) {
InvalidFloatToInt,
InvalidMinimumMaximumType,
NonExhaustiveSchemaValidators,
} || Allocator.Error;
} || Allocator.Error || @typeInfo(@typeInfo(@TypeOf(regex.Regex.compile)).Fn.return_type.?).ErrorUnion.error_set;

/// Error relating to the validation of JSON data against the schema
pub const ValidateError = error{
/// TODO top level compiler
TODOTopLevel,
};
} || @typeInfo(@typeInfo(@TypeOf(regex.Regex.partialMatch)).Fn.return_type.?).ErrorUnion.error_set;

///
/// Compile the provided JSON schema into a more refined form for faster validation.
Expand Down Expand Up @@ -269,15 +399,46 @@ pub const Schema = union(enum) {
if (min_items_schema != null or max_items_schema != null) {
const sub_schema = Schema{ .MinMaxItems = try MinMaxItems.compile(min_items_schema, max_items_schema) };
try schema_list.append(sub_schema);
schema_used += 1;
if (min_items_schema) |_| {
schema_used += 1;
}
if (max_items_schema) |_| {
schema_used += 1;
}
}

const minimum_schema = object.get("minimum");
const maximum_schema = object.get("maximum");
if (minimum_schema != null or maximum_schema != null) {
const sub_schema = Schema{ .MinimumMaximum = try MinimumMaximum.compile(minimum_schema, maximum_schema) };
try schema_list.append(sub_schema);
schema_used += 1;
if (minimum_schema) |_| {
schema_used += 1;
}
if (maximum_schema) |_| {
schema_used += 1;
}
}

const properties = object.get("properties");
const pattern_properties = object.get("patternProperties");
const additional_properties = object.get("additionalProperties");
const required = object.get("required");
if (properties != null or pattern_properties != null or pattern_properties != null or additional_properties != null or required != null) {
const sub_schema = Schema{ .PatternMatch = try PatternMatch.compile(allocator, properties, pattern_properties, additional_properties, required) };
try schema_list.append(sub_schema);
if (properties) |_| {
schema_used += 1;
}
if (pattern_properties) |_| {
schema_used += 1;
}
if (additional_properties) |_| {
schema_used += 1;
}
if (required) |_| {
schema_used += 1;
}
}

if (object.count() != schema_used) {
Expand All @@ -290,9 +451,35 @@ pub const Schema = union(enum) {
};
}

///
/// Deinitialise the compiled schema.
///
/// Arguments:
/// IN self: Self - The compiled schema.
/// IN allocator: Allocator - The allocator used in Schema.compile()
///
pub fn deinit(self: Self, allocator: Allocator) void {
switch (self) {
.Schemas => |schemas| allocator.free(schemas),
.Schemas => |schemas| {
for (schemas) |elem| {
elem.deinit(allocator);
}
allocator.free(schemas);
},
.PatternMatch => |pattern_match| {
for (pattern_match.pattern.items) |*pattern| {
switch (pattern.pattern) {
.Regex => |*reg| reg.deinit(),
else => {},
}
pattern.matches.deinit(allocator);
}
pattern_match.pattern.deinit();
if (pattern_match.not_matches) |not_matches| {
not_matches.*.deinit(allocator);
allocator.destroy(not_matches);
}
},
else => {},
}
}
Expand Down
11 changes: 7 additions & 4 deletions src/tests.zig
Original file line number Diff line number Diff line change
Expand Up @@ -61,14 +61,17 @@ test "c API" {
}

test "JSON Schema Test Suite" {
const test_files_dir = "JSON-Schema-Test-Suite/tests/draft7/";
const test_files_dir = "libs/JSON-Schema-Test-Suite/tests/draft7/";
const test_files = .{
test_files_dir ++ "boolean_schema.json",
test_files_dir ++ "type.json",
test_files_dir ++ "maximum.json",
test_files_dir ++ "maxItems.json",
test_files_dir ++ "minItems.json",
test_files_dir ++ "minimum.json",
test_files_dir ++ "maximum.json",
test_files_dir ++ "minItems.json",
test_files_dir ++ "patternProperties.json",
test_files_dir ++ "properties.json",
test_files_dir ++ "required.json",
test_files_dir ++ "type.json",
};

inline for (test_files) |test_file| {
Expand Down

0 comments on commit c6769f2

Please sign in to comment.