saphyr 0.4.0.beta

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,129 @@
1
+ require 'json'
2
+
3
+ module Saphyr
4
+
5
+ # Base class used to define a validator.
6
+ class Validator
7
+
8
+ class << self
9
+ attr_accessor :cache_config, :proc_idx
10
+
11
+ def config()
12
+ self.cache_config ||= Saphyr::Schema.new
13
+ end
14
+
15
+ # ----- Proxy all following method calls to Saphyr::Schema instance
16
+ def strict(value)
17
+ config.strict value
18
+ end
19
+
20
+ def root(value)
21
+ config.root value
22
+ end
23
+
24
+ def field(name, type, **opts)
25
+ if config.root_array? and name != :_root_
26
+ raise Saphyr::Error.new "Can only define ':_root_' field when root is ':array'"
27
+ end
28
+ if name == :_root_ and type != :array
29
+ raise Saphyr::Error.new "Field ':_root_' must be of type ':array'"
30
+ end
31
+ config.field name, type, **opts
32
+ end
33
+
34
+ def schema(name, &block)
35
+ config.schema name, &block
36
+ end
37
+
38
+ def conditional(cond, &block)
39
+ #
40
+ # TODO: 'cond' must be a method Symbol or a proc / lambda
41
+ #
42
+ method = cond
43
+ if cond.is_a? Proc
44
+ m = "_proc_#{self.internal_proc_index}".to_sym
45
+ self.send :define_method, m, cond
46
+ method = m
47
+ self.proc_idx += 1
48
+ end
49
+ config.conditional method, &block
50
+ end
51
+
52
+ def cast(field, method)
53
+ if method.is_a? Proc
54
+ m = "_proc_#{self.internal_proc_index}".to_sym
55
+ self.send :define_method, m, method
56
+ method = m
57
+ self.proc_idx += 1
58
+ end
59
+ config.cast field, method
60
+ end
61
+ # ----- / Proxy
62
+
63
+ private
64
+
65
+ def internal_proc_index()
66
+ self.proc_idx ||= 0
67
+ end
68
+ end
69
+
70
+ def initialize()
71
+ @proc_idx = 0
72
+ @ctx = nil
73
+ end
74
+
75
+ # Get the validator configuration (ie: the attached schema)
76
+ # @return [Saphyr::Schema]
77
+ def get_config()
78
+ self.class.config
79
+ end
80
+
81
+ # Find a local schema
82
+ # @param name [Symbol] The schema name.
83
+ # @return [Saphyr::Schema]
84
+ def find_schema(name)
85
+ self.class.config.find_schema name
86
+ end
87
+
88
+ # Get the parsed JSON data.
89
+ # @return [Hash]
90
+ def data()
91
+ return nil if @ctx.nil?
92
+ @ctx.data
93
+ end
94
+
95
+ # -----
96
+
97
+ # Validate an already parsed JSON document.
98
+ # @param [Hash | Array] The data to validate.
99
+ # @return [Boolean] Wheter the validation was successful or failed.
100
+ def validate(data)
101
+ @ctx = Saphyr::Engine::Context.new [self], get_config, data, nil, '//'
102
+ engine = Saphyr::Engine.new @ctx
103
+ engine.validate
104
+ @ctx.errors.size == 0
105
+ end
106
+
107
+ # Parse and validate a JSON document.
108
+ # @param json [String] The JSON document.
109
+ # @return [Boolean] Wheter the validation was successful or failed.
110
+ def parse_and_validate(json)
111
+ validate JSON.parse(json)
112
+ end
113
+
114
+ # Get the validation errors
115
+ # @return [Array] An array of errors.
116
+ def errors()
117
+ return [] if @ctx.nil?
118
+ @ctx.errors
119
+ end
120
+
121
+ # Get a field from the data to validate.
122
+ # @param [String | Symbol] The field name
123
+ # @return The field value
124
+ def get(field)
125
+ data = @ctx.data_to_validate
126
+ data[field.to_s]
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Saphyr
4
+ VERSION = "0.4.0.beta"
5
+ end
data/lib/saphyr.rb ADDED
@@ -0,0 +1,160 @@
1
+ #efrozen_string_literal: true
2
+
3
+ require_relative "saphyr/version"
4
+
5
+ # Saphyr is a libray used to validate JSON data using a schema defined with a DSL.
6
+ #
7
+ # == Usage example :
8
+ #
9
+ # Say you have a JSON literal string or already parsed 'data' like that :
10
+ #
11
+ # json = '{"id": 3465, "name": "Bob"}'
12
+ #
13
+ # data = {
14
+ # "id" => 3465,
15
+ # "name" => "Bob",
16
+ # }
17
+ #
18
+ # Define a Validator with a specific schema :
19
+ #
20
+ # class ItemValidator < Saphyr::Validator
21
+ # schema do
22
+ # field :id, :integer, gte: 1, lt: 32000
23
+ # field :name, :string, min: 2, max: 50
24
+ # end
25
+ # end
26
+ #
27
+ # Validate data :
28
+ #
29
+ # v = ItemValidator.new
30
+ # if v.validate data
31
+ # puts "Validation : SUCCESS", "\n"
32
+ # else
33
+ # puts "Validation : FAILED", "\n"
34
+ # Saphyr::Helpers::Format.errors_to_text v.errors
35
+ # end
36
+ #
37
+ # Or :
38
+ #
39
+ # v = ItemValidator.new
40
+ # if v.parse_and_validate json
41
+ # puts "Validation : SUCCESS", "\n"
42
+ # data = v.data # Get back the parsed json data
43
+ # else
44
+ # puts "Validation : FAILED", "\n"
45
+ # Saphyr::Helpers::Format.errors_to_text v.errors
46
+ # end
47
+ #
48
+ module Saphyr
49
+ class Error < StandardError; end
50
+
51
+ class << self
52
+ # The global configuration.
53
+ # @return [Saphyr::Config]
54
+ attr_accessor :config
55
+
56
+ # This method is part of the DSL used to register global field type or schema.
57
+ # @param block [Block] This block must use the followingDSL methods:
58
+ # 'field_type' and 'schema'
59
+ # @api public
60
+ def register(&block)
61
+ self.config ||= Config.new
62
+ self.config.instance_eval &block
63
+ end
64
+
65
+ # Get a specific global schema.
66
+ # @param name [Symbol] The name of the schema
67
+ # @return [Saphyr::Schema]
68
+ # @raise [Saphyr::Error] If schema does not exists.
69
+ # @api public
70
+ def global_schema(name)
71
+ self.config.get_schema name
72
+ end
73
+ end
74
+
75
+
76
+ # This class is used to encapsulate global settings (field types, schema).
77
+ # @api private
78
+ class Config
79
+ attr_reader :field_types, :schemas
80
+
81
+ def initialize()
82
+ @schemas = {}
83
+ @field_types = {
84
+ array: Saphyr::Fields::ArrayField,
85
+ schema: Saphyr::Fields::SchemaField,
86
+ }
87
+ end
88
+
89
+ # ---------------------------------------------------- DSL
90
+
91
+ # This method is part of the DSL and used to register global field type
92
+ # and global schemas.
93
+ #
94
+ # @param name [String, Symbol] The field type name
95
+ # @param klass [Class] The class used to handle the field type
96
+ def field_type(name, klass)
97
+ raise Saphyr::Error.new "Cannot overwrite ':array' field" if name == :array
98
+ raise Saphyr::Error.new "Cannot overwrite ':schema' field" if name == :schema
99
+ @field_types[name] = klass
100
+ end
101
+
102
+ # This method is part of the DSL used to configure global settings.
103
+ # Register a new global schema.
104
+ #
105
+ # == Parameters:
106
+ # name::
107
+ # The name of the schema
108
+ # can be `Symbol` or `String`.
109
+ # &block::
110
+ # The block evaluated by the DSL.
111
+ def schema(name, &block)
112
+ schema = Saphyr::Schema.new
113
+ schema.instance_eval &block
114
+ @schemas[name] = schema
115
+ end
116
+
117
+ # -------------------------------------------------- / DSL
118
+
119
+ # Instanciate a registered field type.
120
+ # @param type [Symbol] The type name use to register the field type
121
+ # @param opts [Hash] A hash of options to pass to the field type instance.
122
+ # @return [Field Type Object] An instance of the field type.
123
+ def instanciate_field_type(type, opts={})
124
+ klass = @field_types[type]
125
+ raise Saphyr::Error.new "Unknown field : #{type}" if klass.nil?
126
+ Object.const_get(klass.name).new opts
127
+ end
128
+
129
+ # Get a specific global schema given his name.
130
+ # @param name [Symbol] The schema name
131
+ # @return [Saphyr::Schema]
132
+ # @raise [Saphyr::Error] If schema does not exists.
133
+ def get_schema(name)
134
+ raise Saphyr::Error.new "Unknown schema : #{name}" unless @schemas.key? name
135
+ @schemas[name]
136
+ end
137
+ end
138
+ end
139
+
140
+
141
+ #
142
+ # Required files
143
+ #
144
+ require_relative './saphyr/asserts'
145
+ require_relative './saphyr/fields'
146
+ require_relative './saphyr/schema'
147
+ require_relative './saphyr/validator'
148
+ require_relative './saphyr/engine'
149
+ require_relative './saphyr/helpers'
150
+
151
+
152
+ #
153
+ # Register default field types.
154
+ #
155
+ Saphyr.register do
156
+ field_type :string, Saphyr::Fields::StringField
157
+ field_type :integer, Saphyr::Fields::IntegerField
158
+ field_type :float, Saphyr::Fields::FloatField
159
+ field_type :boolean, Saphyr::Fields::BooleanField
160
+ end
@@ -0,0 +1,133 @@
1
+ # How To Define a Schema
2
+
3
+ A validation schema is basicly a class inheriting
4
+ from `Saphyr::Validator` and using the DSL to describe the schema.
5
+
6
+ ```ruby
7
+ data = {
8
+ "name" => 'my item',
9
+ }
10
+
11
+ class ItemValidator < Saphyr::Validator
12
+ # Declaring the 'name' field into the validation schema:
13
+
14
+ field :name, :string, min: 5, max: 50
15
+ # | | |-------------|
16
+ # | | |
17
+ # name type options (depending on the type)
18
+
19
+ # The field name can be a Sring or Symbol:
20
+ field 'name', :string, min: 5, max: 50
21
+ end
22
+ ```
23
+
24
+ By default the `Saphyr` library provide many field types, like: `:integer`,
25
+ `:float`, `:string`, `:array` ...
26
+
27
+ _(You can also define your own custom field type, we'll see that later)_
28
+
29
+ Example of some types:
30
+
31
+ ```ruby
32
+ data = {
33
+ "id" => 721, # Integer: > 0
34
+ "type" => 2, # Integer: possible value 1, 2 or 3
35
+ "price" => 27.60, # Float: > 0
36
+ "name" => 'my item', # String: 5 >= len <= 50
37
+ "active" => true, # Boolean
38
+ }
39
+
40
+ class ItemValidator < Saphyr::Validator
41
+ field :id, :integer, gt: 0
42
+ field :type, :integer, in: [1, 2, 3]
43
+ field :price, :float, gt: 0
44
+ field :name, :string, min: 5, max: 50
45
+ field :active, :boolean
46
+ end
47
+ ```
48
+
49
+ Each `Validator` can embed local schemas used to describe the structure of data:
50
+
51
+ ```ruby
52
+ data = {
53
+ "id" => 721,
54
+ "name" => 'my item',
55
+ "timestamps" => {
56
+ "created_at" => 1669215446, # Unix timestamp
57
+ "modified_at" => 1670943462,
58
+ }
59
+ }
60
+
61
+ class ItemValidator < Saphyr::Validator
62
+ schema :timestamp do
63
+ field :created_at, :integer, gt: 0
64
+ field :modified_at, :integer, gt: 0
65
+ end
66
+
67
+ field :id, :integer, gt: 0
68
+ field :name, :string, min: 5, max: 50
69
+ field :timestamps, :schema, name: :timestamp
70
+ end
71
+ ```
72
+
73
+ The `:timestamp` schema is local to the validator and cannot be accessed by other
74
+ validators.
75
+
76
+ If you need to share a schema between many validators then you can declare it globally:
77
+
78
+ ```ruby
79
+ Saphyr.register do
80
+ schema :timestamp do
81
+ field :created_at, :integer, gt: 0
82
+ field :modified_at, :integer, gt: 0
83
+ end
84
+ end
85
+
86
+ class ItemValidator < Saphyr::Validator
87
+ # ....
88
+ field :timestamps, :schema, name: :timestamp
89
+ end
90
+
91
+ class PostValidator < Saphyr::Validator
92
+ # ....
93
+ field :timestamps, :schema, name: :timestamp
94
+ end
95
+ ```
96
+
97
+ ## When root is an array
98
+
99
+ By default validator root are set to `:object`, but this can be customized.
100
+
101
+ In this case, only one virtual field must be defined : `:_root_` and it must be of type `:array`
102
+
103
+ Example with `:of_type` :
104
+
105
+ ```ruby
106
+ data = ['fr', 'en', 'es']
107
+
108
+ class ItemValidator < Saphyr::Validator
109
+ root :array
110
+
111
+ field :_root_, :array, min: 2, max: 5, of_type: :string, opts: {len: 2}
112
+ end
113
+ ```
114
+
115
+ Example with `:of_schema` :
116
+
117
+ ```ruby
118
+ data = [
119
+ { "id" => 12, "label" => "tag1" },
120
+ { "id" => 15, "label" => "tag2" },
121
+ ]
122
+
123
+ class ItemValidator < Saphyr::Validator
124
+ root :array
125
+
126
+ schema :tag do
127
+ field :id, :integer, gt: 0
128
+ field :label, :string, min: 2, max: 30
129
+ end
130
+
131
+ field :_root_, :array, min: 2, max: 4, of_schema: :tag
132
+ end
133
+ ```
@@ -0,0 +1,210 @@
1
+ # An Overview of Library Field Types
2
+
3
+ By default the `Saphyr` library is including many field types.
4
+
5
+ ## Common options
6
+
7
+ All field type have the common `:required`, `:nullable`.
8
+
9
+ ## String
10
+
11
+ Authorized options for the `:string` type: `[:eq, :len, :min, :max, :in, :regexp]`
12
+
13
+ Here is an example with all possible options for `:string` type:
14
+
15
+ ```ruby
16
+ class MyValidator < Saphyr::Validator
17
+ field :name, :string
18
+ field :name, :string, eq: 'v1.1'
19
+ field :name, :string, min: 5, max: 50
20
+ field :name, :string, max: 50
21
+ field :name, :string, len: 15
22
+ field :name, :string, len: 15, regexp: /^[a-f0-9]+$/
23
+ field :name, :string, regexp: /^[A-Z0-9]{15}$/
24
+ field :name, :string, in: ['jpg', 'png', 'gif']
25
+
26
+
27
+ field :location, :string, required: false, min: 10
28
+ field :info, :string, nullable: true, max: 1024
29
+ end
30
+ ```
31
+
32
+ - If you use `:eq` option then you cannot use any of the other options
33
+ - If you use `:len` option then you cannot use `:min` and `:max` options
34
+ - If you use `:in` option then you cannot use any of the other options
35
+
36
+
37
+ ## Integer
38
+
39
+ Authorized options for the `:integer` type: `[:eq, :gt, :gte, :lt, :lte, :in]`
40
+
41
+ Here is an example with all possible options for `:integer` type:
42
+
43
+ ```ruby
44
+ class MyValidator < Saphyr::Validator
45
+ field :count, :integer
46
+ field :count, :integer, eq: 'v1.1'
47
+ field :count, :integer, gt: 0
48
+ field :count, :integer, lt: 50
49
+ field :count, :integer, gte: 5, lte: 50
50
+ field :count, :integer, in: ['jpg', 'png', 'gif']
51
+
52
+ field :count, :integer, required: false, gte: 10
53
+ field :count, :integer, nullable: true, lte: 1024
54
+ end
55
+ ```
56
+
57
+ - If you use `:eq` option then you cannot use any of the other options
58
+ - If you use `:in` option then you cannot use any of the other options
59
+
60
+ ## Float
61
+
62
+ Authorized options for the `:float` type: `[:eq, :gt, :gte, :lt, :lte, :in]`
63
+
64
+ Here is an example with all possible options for `:float` type:
65
+
66
+ ```ruby
67
+ class MyValidator < Saphyr::Validator
68
+ field :price, :float
69
+ field :price, :float, eq: 15.1
70
+ field :price, :float, gt: 0
71
+ field :price, :float, lt: 50
72
+ field :price, :float, gte: 5, lte: 50
73
+ field :price, :float, in: ['jpg', 'png', 'gif']
74
+
75
+ field :price, :float, required: false, gte: 10
76
+ field :price, :float, nullable: true, lte: 1024
77
+ end
78
+ ```
79
+
80
+ - If you use `:eq` option then you cannot use any of the other options
81
+ - If you use `:in` option then you cannot use any of the other options
82
+
83
+ ## Boolean
84
+
85
+ Authorized options for the `:boolean` type: `[:eq]`
86
+
87
+ Here is an example with all possible options for `:boolean` type:
88
+
89
+ ```ruby
90
+ class MyValidator < Saphyr::Validator
91
+ field :active, :boolean
92
+ field :active, :boolean, eq: true
93
+ field :active, :boolean, eq: false
94
+
95
+ field :active, :boolean, required: false
96
+ field :active, :boolean, nullable: true
97
+ end
98
+ ```
99
+
100
+ ## Array
101
+
102
+ Authorized options for the `:array` type: `[:len, :min, :max, :of_type, :of_schema, :opts]`
103
+
104
+ The `:array` type is little bit different than the other types.
105
+
106
+ The following options `[:len, :min, :max]` are for the array size then you must define
107
+ the type of the array element, this is where `:of_type` and `:of_schema` options
108
+ take place.
109
+
110
+ - One of this option is required: `:of_type` and `:of_schema`
111
+ - If you use `:len` option then you cannot use `:min`, `:max` options
112
+
113
+ ### Example with `of_type`:
114
+
115
+ ```ruby
116
+ data = {
117
+ 'tags' => ['code', 'ruby', 'json']
118
+ }
119
+
120
+ class MyValidator < Saphyr::Validator
121
+ field :tags, :array, of_type: :string, opts: {max: 50}
122
+ end
123
+
124
+ class MyValidator < Saphyr::Validator
125
+ field :tags, :array, min: 1, max: 10, of_type: :string, opts: {max: 50}
126
+ # |-------------| |-------------|
127
+ # | |
128
+ # Size of array must be: 1 >= s <= 10 |
129
+ # |
130
+ # This 'opts' are for the element of array, ie: 'string'
131
+ end
132
+ ```
133
+
134
+ ### Example with `of_schema`:
135
+
136
+ - When using `:of_schema` then `:opts` cannot be used
137
+
138
+ ```ruby
139
+ data = {
140
+ 'code' => 'AGF30',
141
+ 'tags' => [
142
+ {'id' => 234, 'label' => 'ruby'},
143
+ {'id' => 567, 'label' => 'elixir'}
144
+ ]
145
+ }
146
+
147
+ class MyValidator < Saphyr::Validator
148
+ schema :tag do
149
+ field :id, :integer, gt: 0
150
+ field :label, :string, min: 5, max: 30
151
+ end
152
+
153
+ field :code, :string, min: 5, max: 10
154
+ field :tags, :array, of_schema: :tag
155
+ end
156
+ ```
157
+
158
+ ## Schema
159
+
160
+ The schema field type does not have any option.
161
+
162
+ It is used to describe the structure hierarchy of the document. The referenced schema
163
+ can local to the validator or global to `Saphyr` library.
164
+
165
+ To find the schema, we first search it in the validator, if there isn't the named schema in
166
+ the validtor then you search it in the global `Saphyr` registry.
167
+ _(and if not found an exception is raised)_
168
+
169
+ Using a local validator schema:
170
+
171
+ ```ruby
172
+ data = {
173
+ # ....
174
+ "timestamps": {
175
+ "created_at": 1669215446, # Unix timestamp
176
+ "modified_at": 1670943462
177
+ }
178
+ }
179
+
180
+ class ItemValidator < Saphyr::Validator
181
+ schema :timestamp do
182
+ field :created_at, :integer, gt: 0
183
+ field :modified_at, :integer, gt: 0
184
+ end
185
+
186
+ # ....
187
+ field :timestamps, :schema, name: :timestamp
188
+ end
189
+ ```
190
+
191
+ Using a global schema:
192
+
193
+ ```ruby
194
+ Saphyr.register do
195
+ schema :timestamp do
196
+ field :created_at, :integer, gt: 0
197
+ field :modified_at, :integer, gt: 0
198
+ end
199
+ end
200
+
201
+ class ItemValidator < Saphyr::Validator
202
+ # ....
203
+ field :timestamps, :schema, name: :timestamp
204
+ end
205
+
206
+ class PostValidator < Saphyr::Validator
207
+ # ....
208
+ field :timestamps, :schema, name: :timestamp
209
+ end
210
+ ```
@@ -0,0 +1,80 @@
1
+ # How to Use Strict Mode
2
+
3
+ By default the library is setup as strict mode, this mean that all fields from schema or
4
+ data must be described.
5
+
6
+ ```ruby
7
+ data = {
8
+ "name" => 'my item',
9
+ "info" => 'Lipsum', # Not defined in schema
10
+ }
11
+
12
+ class ItemValidator < Saphyr::Validator
13
+ field :id, :integer, gt: 0 # Not existing in data
14
+ field :name, :string, min: 5, max: 50
15
+ end
16
+ ```
17
+
18
+ when trying to validate the data, we will get 2 errors:
19
+
20
+ ```ruby
21
+ v = ItemValidator.new
22
+ v.validate data
23
+ puts "Validation : FAILED", "\n"
24
+ Saphyr::Helpers::Format.errors_to_text validator.errors
25
+ ```
26
+
27
+ Output:
28
+
29
+ ```
30
+ Validation : FAILED
31
+
32
+ path: //id
33
+ - type: strict_mode:missing_in_data
34
+ - data: {:field=>"id"}
35
+ - msg: Missing fields in data
36
+
37
+ path: //info
38
+ - type: strict_mode:missing_in_schema
39
+ - data: {:field=>"info"}
40
+ - msg: Missing fields in schema
41
+ ```
42
+
43
+ ### Disabling the strict mode
44
+
45
+ ```ruby
46
+ class ItemValidator < Saphyr::Validator
47
+ strict false
48
+
49
+ field :id, :integer, gt: 0
50
+ field :name, :string, min: 5, max: 50
51
+ end
52
+ ```
53
+
54
+ In this case the validation wil succeed.
55
+
56
+ ### Combining the ':required' option with strict mode
57
+
58
+ ```ruby
59
+
60
+ class ItemValidator < Saphyr::Validator
61
+ field :id, :integer, gt: 0
62
+ field :name, :string, min: 5, max: 50
63
+ field :info, :string, min: 5, max: 50, required: false
64
+ end
65
+ ```
66
+
67
+ This will ensure that the following data example are both valid:
68
+
69
+ ```ruby
70
+ data1 = {
71
+ "id" => 235,
72
+ "name" => 'my item',
73
+ }
74
+
75
+ data2 = {
76
+ "id" => 235,
77
+ "name" => 'my item',
78
+ "info" => 'Lipsum ...',
79
+ }
80
+ ```