json-guard 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,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JsonGuard
4
+ class Property
5
+ attr_accessor :name, :type, :required, :message, :messages, :i18n_key,
6
+ :message_template, :enum, :format, :pattern, :minimum, :maximum,
7
+ :null_allowed, :schema
8
+
9
+ def initialize(name, type, options = {})
10
+ @name = name
11
+ @type = type
12
+ @required = options[:required] || false
13
+ @null_allowed = options[:null] || false
14
+ @message = options[:message]
15
+ @messages = options[:messages] || {}
16
+ @i18n_key = options[:i18n_key]
17
+ @message_template = options[:message_template]
18
+ @enum = options[:enum]
19
+ @format = options[:format]
20
+ @pattern = options[:pattern]
21
+ @minimum = options[:minimum]
22
+ @maximum = options[:maximum]
23
+ @schema = options[:schema]
24
+ end
25
+
26
+ def error_message(validation_type, record = nil, context = nil)
27
+ # Priority order:
28
+ # 1. Dynamic message (Proc)
29
+ # 2. Specific message for validation type
30
+ # 3. General message
31
+ # 4. I18n translation
32
+ # 5. Message template
33
+ # 6. Default message
34
+
35
+ if @message.is_a?(Proc)
36
+ return @message.call(record, nil, context)
37
+ end
38
+
39
+ if @messages[validation_type]
40
+ return @messages[validation_type]
41
+ end
42
+
43
+ if @message
44
+ return @message
45
+ end
46
+
47
+ if @i18n_key && defined?(I18n)
48
+ return I18n.t("json_guard.errors.#{@i18n_key}.#{validation_type}",
49
+ default: I18n.t("json_guard.errors.#{validation_type}",
50
+ default: default_message(validation_type)))
51
+ end
52
+
53
+ if @message_template
54
+ return interpolate_template(@message_template, record, context)
55
+ end
56
+
57
+ # Default fallback
58
+ default_message(validation_type)
59
+ end
60
+
61
+ private
62
+
63
+ def interpolate_template(template, record, context)
64
+ variables = {
65
+ operation: context&.to_s || 'operation',
66
+ current_time: Time.current.strftime('%Y-%m-%d %H:%M:%S'),
67
+ model_name: record&.class&.name&.downcase || 'record'
68
+ }
69
+ template % variables
70
+ end
71
+
72
+ def default_message(validation_type)
73
+ field_name = @name.to_s.gsub('_', ' ').capitalize
74
+
75
+ case validation_type
76
+ when :required
77
+ "#{field_name} is required"
78
+ when :invalid_type
79
+ type_str = @type.is_a?(Array) ? @type.join(' or ') : @type.to_s
80
+ "#{field_name} must be a #{type_str}"
81
+ when :invalid_enum
82
+ "#{field_name} must be one of: #{@enum.join(', ')}" if @enum
83
+ when :invalid_format
84
+ "#{field_name} format is invalid"
85
+ when :invalid_pattern
86
+ "#{field_name} format is invalid"
87
+ when :too_small
88
+ "#{field_name} must be greater than or equal to #{@minimum}"
89
+ when :too_large
90
+ "#{field_name} must be less than or equal to #{@maximum}"
91
+ else
92
+ "#{field_name} is invalid"
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/railtie"
4
+
5
+ module JsonGuard
6
+ class Railtie < Rails::Railtie
7
+ initializer "json_guard.initialize" do
8
+ ActiveSupport.on_load(:active_record) do
9
+ require "json_guard/active_record_extension"
10
+ extend JsonGuard::ActiveRecordExtension
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JsonGuard
4
+ class Schema
5
+ attr_reader :properties, :contexts
6
+
7
+ def initialize
8
+ @properties = {}
9
+ @contexts = {}
10
+ end
11
+
12
+ class << self
13
+ def inherited(subclass)
14
+ super
15
+ subclass.instance_variable_set(:@properties, {})
16
+ subclass.instance_variable_set(:@contexts, {})
17
+ end
18
+
19
+ def properties
20
+ @properties ||= {}
21
+ end
22
+
23
+ def contexts
24
+ @contexts ||= {}
25
+ end
26
+
27
+ def context(name, &block)
28
+ @contexts[name] = block
29
+ end
30
+
31
+ def validate(data, context = nil)
32
+ validator = Validator.new(self)
33
+ validator.validate(data, context)
34
+ end
35
+
36
+ # DSL methods
37
+ def method_missing(method_name, *args, &block)
38
+ if block_given?
39
+ # Object definition
40
+ define_object_property(method_name, args.first || {}, &block)
41
+ else
42
+ # Simple property definition
43
+ define_property(method_name, args[0], args[1] || {})
44
+ end
45
+ end
46
+
47
+ def respond_to_missing?(method_name, include_private = false)
48
+ true
49
+ end
50
+
51
+ private
52
+
53
+ def define_property(name, type, options = {})
54
+ # Handle multiple types like [:string, :null]
55
+ if type.is_a?(Array)
56
+ options[:null] = type.include?(:null)
57
+ type = type.reject { |t| t == :null }.first
58
+ end
59
+
60
+ property = Property.new(name, type, options)
61
+ @properties[name] = property
62
+ end
63
+
64
+ def define_object_property(name, options = {}, &block)
65
+ # Handle multiple types for objects like [:object, :null]
66
+ if options.is_a?(Array)
67
+ type_array = options
68
+ options = {}
69
+ options[:null] = type_array.include?(:null)
70
+ options[:type] = type_array.reject { |t| t == :null }.first || :object
71
+ else
72
+ options[:type] ||= :object
73
+ end
74
+
75
+ # Create nested schema class
76
+ nested_schema = Class.new(Schema)
77
+ nested_schema.class_eval(&block) if block
78
+
79
+ property = Property.new(name, options[:type], options.merge(schema: nested_schema))
80
+ @properties[name] = property
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,145 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'uri'
4
+ require 'date'
5
+ require 'json'
6
+
7
+ module JsonGuard
8
+ class Validator
9
+ attr_reader :schema, :errors
10
+
11
+ def initialize(schema)
12
+ @schema = schema
13
+ @errors = []
14
+ end
15
+
16
+ def validate(data, context = nil)
17
+ @errors = []
18
+
19
+ return false if data.nil?
20
+
21
+ validate_properties(data, @schema.properties, context)
22
+
23
+ @errors.empty?
24
+ end
25
+
26
+ def error_messages
27
+ @errors.map { |error| error[:message] }
28
+ end
29
+
30
+ def detailed_errors
31
+ @errors
32
+ end
33
+
34
+ private
35
+
36
+ def validate_properties(data, properties, context = nil, path = [])
37
+ properties.each do |name, property|
38
+ current_path = path + [name.to_s]
39
+ value = data[name.to_s] || data[name.to_sym]
40
+
41
+ validate_property(property, value, current_path, context)
42
+ end
43
+ end
44
+
45
+ def validate_property(property, value, path, context)
46
+ # Check if value is present
47
+ if value.nil?
48
+ if property.required
49
+ add_error(property, :required, path, context)
50
+ end
51
+ return unless property.null_allowed
52
+ end
53
+
54
+ # Skip validation if value is nil and null is allowed
55
+ return if value.nil? && property.null_allowed
56
+
57
+ # Type validation
58
+ unless valid_type?(value, property.type)
59
+ add_error(property, :invalid_type, path, context)
60
+ return
61
+ end
62
+
63
+ # Enum validation
64
+ if property.enum && !property.enum.include?(value)
65
+ add_error(property, :invalid_enum, path, context)
66
+ end
67
+
68
+ # Format validation
69
+ if property.format && !valid_format?(value, property.format)
70
+ add_error(property, :invalid_format, path, context)
71
+ end
72
+
73
+ # Pattern validation
74
+ if property.pattern && !property.pattern.match?(value.to_s)
75
+ add_error(property, :invalid_pattern, path, context)
76
+ end
77
+
78
+ # Numeric validations
79
+ if property.minimum && value < property.minimum
80
+ add_error(property, :too_small, path, context)
81
+ end
82
+
83
+ if property.maximum && value > property.maximum
84
+ add_error(property, :too_large, path, context)
85
+ end
86
+
87
+ # Nested object validation
88
+ if property.schema && property.type == :object && value.is_a?(Hash)
89
+ nested_validator = Validator.new(property.schema)
90
+ nested_validator.validate(value, context)
91
+
92
+ nested_validator.detailed_errors.each do |error|
93
+ nested_path = path + error[:path].split('.')
94
+ @errors << error.merge(path: nested_path.join('.'))
95
+ end
96
+ end
97
+ end
98
+
99
+ def valid_type?(value, type)
100
+ case type
101
+ when :string
102
+ value.is_a?(String)
103
+ when :integer
104
+ value.is_a?(Integer)
105
+ when :boolean
106
+ value.is_a?(TrueClass) || value.is_a?(FalseClass)
107
+ when :array
108
+ value.is_a?(Array)
109
+ when :object
110
+ value.is_a?(Hash)
111
+ when :null
112
+ value.nil?
113
+ else
114
+ true
115
+ end
116
+ end
117
+
118
+ def valid_format?(value, format)
119
+ case format
120
+ when :email
121
+ value =~ /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
122
+ when :uri, :url
123
+ value =~ /\A#{URI::DEFAULT_PARSER.make_regexp}\z/
124
+ when :date
125
+ Date.parse(value) rescue false
126
+ when :datetime
127
+ DateTime.parse(value) rescue false
128
+ else
129
+ true
130
+ end
131
+ end
132
+
133
+ def add_error(property, validation_type, path, context, record = nil)
134
+ message = property.error_message(validation_type, record, context)
135
+
136
+ @errors << {
137
+ path: path.join('.'),
138
+ message: message,
139
+ validation_type: validation_type,
140
+ property: property.name,
141
+ code: validation_type.to_s.upcase
142
+ }
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JsonGuard
4
+ VERSION = "0.1.0"
5
+ end
data/lib/json_guard.rb ADDED
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "json_guard/version"
4
+ require_relative "json_guard/configuration"
5
+ require_relative "json_guard/schema"
6
+ require_relative "json_guard/property"
7
+ require_relative "json_guard/validator"
8
+ require_relative "json_guard/errors"
9
+
10
+ module JsonGuard
11
+ class << self
12
+ def configure
13
+ yield(configuration)
14
+ end
15
+
16
+ def configuration
17
+ @configuration ||= Configuration.new
18
+ end
19
+ end
20
+ end
21
+
22
+ # Load Rails integration if Rails is present
23
+ if defined?(Rails)
24
+ require_relative "json_guard/railtie"
25
+ end
metadata ADDED
@@ -0,0 +1,231 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: json-guard
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Zaid Saeed
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: rails
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '6.0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '6.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: json-schema
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '4.0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '4.0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: activesupport
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '6.0'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '6.0'
54
+ - !ruby/object:Gem::Dependency
55
+ name: bundler
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '2.0'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '2.0'
68
+ - !ruby/object:Gem::Dependency
69
+ name: rake
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '13.0'
75
+ type: :development
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '13.0'
82
+ - !ruby/object:Gem::Dependency
83
+ name: rspec
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: '3.0'
89
+ type: :development
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: '3.0'
96
+ - !ruby/object:Gem::Dependency
97
+ name: rspec-rails
98
+ requirement: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - "~>"
101
+ - !ruby/object:Gem::Version
102
+ version: '6.0'
103
+ type: :development
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - "~>"
108
+ - !ruby/object:Gem::Version
109
+ version: '6.0'
110
+ - !ruby/object:Gem::Dependency
111
+ name: sqlite3
112
+ requirement: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - "~>"
115
+ - !ruby/object:Gem::Version
116
+ version: '1.4'
117
+ type: :development
118
+ prerelease: false
119
+ version_requirements: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - "~>"
122
+ - !ruby/object:Gem::Version
123
+ version: '1.4'
124
+ - !ruby/object:Gem::Dependency
125
+ name: pry
126
+ requirement: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - "~>"
129
+ - !ruby/object:Gem::Version
130
+ version: '0.14'
131
+ type: :development
132
+ prerelease: false
133
+ version_requirements: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - "~>"
136
+ - !ruby/object:Gem::Version
137
+ version: '0.14'
138
+ - !ruby/object:Gem::Dependency
139
+ name: rubocop
140
+ requirement: !ruby/object:Gem::Requirement
141
+ requirements:
142
+ - - "~>"
143
+ - !ruby/object:Gem::Version
144
+ version: '1.50'
145
+ type: :development
146
+ prerelease: false
147
+ version_requirements: !ruby/object:Gem::Requirement
148
+ requirements:
149
+ - - "~>"
150
+ - !ruby/object:Gem::Version
151
+ version: '1.50'
152
+ - !ruby/object:Gem::Dependency
153
+ name: rubocop-rails
154
+ requirement: !ruby/object:Gem::Requirement
155
+ requirements:
156
+ - - "~>"
157
+ - !ruby/object:Gem::Version
158
+ version: '2.19'
159
+ type: :development
160
+ prerelease: false
161
+ version_requirements: !ruby/object:Gem::Requirement
162
+ requirements:
163
+ - - "~>"
164
+ - !ruby/object:Gem::Version
165
+ version: '2.19'
166
+ - !ruby/object:Gem::Dependency
167
+ name: simplecov
168
+ requirement: !ruby/object:Gem::Requirement
169
+ requirements:
170
+ - - "~>"
171
+ - !ruby/object:Gem::Version
172
+ version: '0.22'
173
+ type: :development
174
+ prerelease: false
175
+ version_requirements: !ruby/object:Gem::Requirement
176
+ requirements:
177
+ - - "~>"
178
+ - !ruby/object:Gem::Version
179
+ version: '0.22'
180
+ description: Beautiful JSON Schema validation with Rails-native syntax, context-aware
181
+ rules, and production-ready monitoring
182
+ email:
183
+ - izaidsaeed@gmail.com
184
+ executables: []
185
+ extensions: []
186
+ extra_rdoc_files: []
187
+ files:
188
+ - CHANGELOG.md
189
+ - CONTRIBUTING.md
190
+ - Gemfile
191
+ - Gemfile.lock
192
+ - LICENSE
193
+ - README.md
194
+ - Rakefile
195
+ - lib/json_guard.rb
196
+ - lib/json_guard/active_record_extension.rb
197
+ - lib/json_guard/configuration.rb
198
+ - lib/json_guard/errors.rb
199
+ - lib/json_guard/property.rb
200
+ - lib/json_guard/railtie.rb
201
+ - lib/json_guard/schema.rb
202
+ - lib/json_guard/validator.rb
203
+ - lib/json_guard/version.rb
204
+ homepage: https://github.com/zaid-4/json-guard
205
+ licenses:
206
+ - MIT
207
+ metadata:
208
+ allowed_push_host: https://rubygems.org
209
+ homepage_uri: https://github.com/zaid-4/json-guard
210
+ source_code_uri: https://github.com/zaid-4/json-guard
211
+ changelog_uri: https://github.com/zaid-4/json-guard/blob/main/CHANGELOG.md
212
+ documentation_uri: https://github.com/zaid-4/json-guard/tree/main/examples
213
+ bug_tracker_uri: https://github.com/zaid-4/json-guard/issues
214
+ rdoc_options: []
215
+ require_paths:
216
+ - lib
217
+ required_ruby_version: !ruby/object:Gem::Requirement
218
+ requirements:
219
+ - - ">="
220
+ - !ruby/object:Gem::Version
221
+ version: 2.7.0
222
+ required_rubygems_version: !ruby/object:Gem::Requirement
223
+ requirements:
224
+ - - ">="
225
+ - !ruby/object:Gem::Version
226
+ version: '0'
227
+ requirements: []
228
+ rubygems_version: 3.6.7
229
+ specification_version: 4
230
+ summary: Enterprise-grade JSON Schema validation for Rails
231
+ test_files: []