senko 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Senko
4
+ VERSION = '0.1.0'
5
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Senko
4
+ class Vocabulary
5
+ attr_reader :custom_keywords
6
+
7
+ def initialize(custom_keywords = {})
8
+ @custom_keywords = custom_keywords || {}
9
+ end
10
+
11
+ def custom?(keyword)
12
+ @custom_keywords.key?(keyword)
13
+ end
14
+
15
+ def validator_for(keyword)
16
+ @custom_keywords[keyword]
17
+ end
18
+
19
+ def register(keyword, validator)
20
+ raise ArgumentError, 'keyword validator must respond to call' unless validator.respond_to?(:call)
21
+
22
+ @custom_keywords[keyword.to_s] = validator
23
+ end
24
+ end
25
+ end
data/lib/senko.rb ADDED
@@ -0,0 +1,171 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ require_relative 'senko/cache'
6
+ require_relative 'senko/code_generator'
7
+ require_relative 'senko/compiler'
8
+ require_relative 'senko/dialect'
9
+ require_relative 'senko/errors'
10
+ require_relative 'senko/format'
11
+ require_relative 'senko/native'
12
+ require_relative 'senko/result'
13
+ require_relative 'senko/schema'
14
+ require_relative 'senko/validator'
15
+ require_relative 'senko/version'
16
+ require_relative 'senko/vocabulary'
17
+
18
+ module Senko
19
+ GLOBAL_CACHE = Cache.new
20
+ GLOBAL_FORMATS = {}
21
+ GLOBAL_KEYWORDS = {}
22
+ UNSET = Object.new
23
+ @registry_version = 0
24
+
25
+ module_function
26
+
27
+ def compile(schema = UNSET, **options)
28
+ schema, options = split_schema_keywords(options) if schema.equal?(UNSET)
29
+
30
+ merged_options = merged_options(options)
31
+ if cacheable?(schema, merged_options)
32
+ GLOBAL_CACHE.fetch(cache_key(schema, merged_options)) { build_schema(schema, merged_options) }
33
+ else
34
+ build_schema(schema, merged_options)
35
+ end
36
+ end
37
+
38
+ def compile_file(path, **options)
39
+ compile(JSON.parse(File.read(path)), **options)
40
+ end
41
+
42
+ def valid?(schema, data, **options)
43
+ compile(schema, **options).valid?(data)
44
+ end
45
+
46
+ def validate(schema, data, **options)
47
+ compile(schema, **options).validate(data)
48
+ end
49
+
50
+ def valid_json?(schema, json, **options)
51
+ compile(schema, **options).valid_json?(json)
52
+ end
53
+
54
+ def validate_json(schema, json, **options)
55
+ compile(schema, **options).validate_json(json)
56
+ end
57
+
58
+ def clear_cache!
59
+ GLOBAL_CACHE.clear
60
+ nil
61
+ end
62
+
63
+ def register_format(name, callable = nil, &block)
64
+ validator = callable || block
65
+ raise ArgumentError, 'format validator must respond to call' unless validator.respond_to?(:call)
66
+
67
+ GLOBAL_FORMATS[name.to_s] = validator
68
+ touch_registry!
69
+ end
70
+
71
+ def register_keyword(name, callable = nil, &block)
72
+ validator = callable || block
73
+ raise ArgumentError, 'keyword validator must respond to call' unless validator.respond_to?(:call)
74
+
75
+ GLOBAL_KEYWORDS[name.to_s] = validator
76
+ touch_registry!
77
+ end
78
+
79
+ def clear_registry!
80
+ GLOBAL_FORMATS.clear
81
+ GLOBAL_KEYWORDS.clear
82
+ touch_registry!
83
+ end
84
+
85
+ def registry_version
86
+ @registry_version
87
+ end
88
+
89
+ def split_schema_keywords(keywords)
90
+ schema = {}
91
+ options = {}
92
+
93
+ keywords.each do |key, value|
94
+ if key.is_a?(Symbol) && Compiler::DEFAULT_OPTIONS.key?(key)
95
+ options[key] = value
96
+ else
97
+ schema[key] = value
98
+ end
99
+ end
100
+
101
+ return [keywords, {}] if schema.empty?
102
+
103
+ [schema, options]
104
+ end
105
+ private_class_method :split_schema_keywords
106
+
107
+ def merged_options(options)
108
+ merged = Compiler::DEFAULT_OPTIONS.merge(options)
109
+ merged[:custom_formats] = GLOBAL_FORMATS.merge(merged[:custom_formats] || {})
110
+ merged[:custom_keywords] = GLOBAL_KEYWORDS.merge(merged[:custom_keywords] || {})
111
+ merged[:registry_version] = registry_version
112
+ merged
113
+ end
114
+ private_class_method :merged_options
115
+
116
+ def touch_registry!
117
+ @registry_version += 1
118
+ clear_cache!
119
+ nil
120
+ end
121
+ private_class_method :touch_registry!
122
+
123
+ def build_schema(schema, options)
124
+ instructions = Compiler.new(options).compile(schema)
125
+ generated_validator = CodeGenerator.generate(schema) unless options[:codegen] == false
126
+ Schema.new(
127
+ source: schema,
128
+ instructions: instructions,
129
+ options: options,
130
+ generated_validator: generated_validator
131
+ )
132
+ end
133
+ private_class_method :build_schema
134
+
135
+ def cacheable?(schema, options)
136
+ !contains_callable?(schema) && !contains_callable?(options)
137
+ end
138
+ private_class_method :cacheable?
139
+
140
+ def contains_callable?(value)
141
+ if value.respond_to?(:call)
142
+ true
143
+ elsif value.is_a?(Hash)
144
+ value.any? { |key, child| contains_callable?(key) || contains_callable?(child) }
145
+ elsif value.is_a?(Array)
146
+ value.any? { |child| contains_callable?(child) }
147
+ else
148
+ false
149
+ end
150
+ end
151
+ private_class_method :contains_callable?
152
+
153
+ def cache_key(schema, options)
154
+ JSON.generate(canonical([schema, options]))
155
+ end
156
+ private_class_method :cache_key
157
+
158
+ def canonical(value)
159
+ case value
160
+ when Hash
161
+ value.keys.sort_by(&:to_s).map { |key| [key.to_s, canonical(value[key])] }
162
+ when Array
163
+ value.map { |child| canonical(child) }
164
+ when Symbol
165
+ value.to_s
166
+ else
167
+ value
168
+ end
169
+ end
170
+ private_class_method :canonical
171
+ end
data/sig/senko.rbs ADDED
@@ -0,0 +1,45 @@
1
+ module Senko
2
+ VERSION: String
3
+
4
+ def self.compile: (Hash[String, untyped] | bool schema, **untyped options) -> Schema
5
+ def self.compile_file: (String path, **untyped options) -> Schema
6
+ def self.valid?: (Hash[String, untyped] | bool schema, untyped data, **untyped options) -> bool
7
+ def self.validate: (Hash[String, untyped] | bool schema, untyped data, **untyped options) -> Result
8
+ def self.valid_json?: (Hash[String, untyped] | bool schema, String json, **untyped options) -> bool
9
+ def self.validate_json: (Hash[String, untyped] | bool schema, String json, **untyped options) -> Result
10
+ def self.clear_cache!: () -> nil
11
+ def self.register_format: (String name, ?untyped callable) { (String) -> bool } -> nil
12
+ def self.register_keyword: (String name, ?untyped callable) { (untyped, untyped) -> bool } -> nil
13
+ def self.clear_registry!: () -> nil
14
+
15
+ class Schema
16
+ def valid?: (untyped data) -> bool
17
+ def validate: (untyped data, ?fail_fast: bool) -> Result
18
+ def valid_json?: (String json) -> bool
19
+ def validate_json: (String json, ?fail_fast: bool) -> Result
20
+ def validate!: (untyped data) -> untyped
21
+ end
22
+
23
+ class Result
24
+ attr_reader errors: Array[Error]
25
+ def valid?: () -> bool
26
+ def to_basic: () -> Hash[String, untyped]
27
+ end
28
+
29
+ class Error
30
+ attr_reader message: String
31
+ attr_reader instance_location: String
32
+ attr_reader keyword_location: String
33
+ attr_reader keyword: String
34
+ end
35
+
36
+ module Native
37
+ def self.type_mask: (untyped value) -> Integer
38
+ def self.string_length: (String value) -> Integer
39
+ def self.numeric_lte?: (Numeric left, Numeric right) -> bool
40
+ def self.numeric_lt?: (Numeric left, Numeric right) -> bool
41
+ def self.numeric_gte?: (Numeric left, Numeric right) -> bool
42
+ def self.numeric_gt?: (Numeric left, Numeric right) -> bool
43
+ def self.unique_array?: (Array[untyped] array) -> bool
44
+ end
45
+ end
metadata ADDED
@@ -0,0 +1,170 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: senko
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Yudai Takada
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: addressable
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: bigdecimal
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: date
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ - !ruby/object:Gem::Dependency
55
+ name: ipaddr
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ type: :runtime
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
68
+ - !ruby/object:Gem::Dependency
69
+ name: json
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ type: :runtime
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
82
+ - !ruby/object:Gem::Dependency
83
+ name: simpleidn
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: '0'
89
+ type: :runtime
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ version: '0'
96
+ - !ruby/object:Gem::Dependency
97
+ name: uri
98
+ requirement: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: '0'
103
+ type: :runtime
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ description: A compiled JSON Schema validator targeting Draft 2020-12 with a pure
111
+ Ruby fallback.
112
+ email:
113
+ - t.yudai92@gmail.com
114
+ executables: []
115
+ extensions:
116
+ - ext/senko/extconf.rb
117
+ extra_rdoc_files: []
118
+ files:
119
+ - Gemfile
120
+ - LICENSE
121
+ - README.md
122
+ - Rakefile
123
+ - ext/senko/extconf.rb
124
+ - ext/senko/instructions.c
125
+ - ext/senko/instructions.h
126
+ - ext/senko/senko.c
127
+ - ext/senko/validator.c
128
+ - ext/senko/validator.h
129
+ - lib/senko.rb
130
+ - lib/senko/cache.rb
131
+ - lib/senko/code_generator.rb
132
+ - lib/senko/compiler.rb
133
+ - lib/senko/compiler/instruction.rb
134
+ - lib/senko/compiler/optimizer.rb
135
+ - lib/senko/compiler/ref_resolver.rb
136
+ - lib/senko/dialect.rb
137
+ - lib/senko/errors.rb
138
+ - lib/senko/format.rb
139
+ - lib/senko/native.rb
140
+ - lib/senko/result.rb
141
+ - lib/senko/schema.rb
142
+ - lib/senko/validator.rb
143
+ - lib/senko/version.rb
144
+ - lib/senko/vocabulary.rb
145
+ - sig/senko.rbs
146
+ homepage: https://example.invalid/senko
147
+ licenses:
148
+ - MIT
149
+ metadata:
150
+ homepage_uri: https://example.invalid/senko
151
+ source_code_uri: https://example.invalid/senko
152
+ rubygems_mfa_required: 'true'
153
+ rdoc_options: []
154
+ require_paths:
155
+ - lib
156
+ required_ruby_version: !ruby/object:Gem::Requirement
157
+ requirements:
158
+ - - ">="
159
+ - !ruby/object:Gem::Version
160
+ version: '3.1'
161
+ required_rubygems_version: !ruby/object:Gem::Requirement
162
+ requirements:
163
+ - - ">="
164
+ - !ruby/object:Gem::Version
165
+ version: '0'
166
+ requirements: []
167
+ rubygems_version: 4.0.6
168
+ specification_version: 4
169
+ summary: Fast JSON Schema validator for Ruby
170
+ test_files: []