json_validator 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 53df7dd58f70876c93e8d4289db52ed1639a0a50
4
+ data.tar.gz: 61f4c218b92cf98902b4298248955f94f2588131
5
+ SHA512:
6
+ metadata.gz: eb4f3e30be96038c7be8501593b1b463bbe7dc89200a8df5885ece06ca136e0b05945caf571d4d9386c1f44b73215eabf0f7252369166c5e6b43ca47ef08ff98
7
+ data.tar.gz: 5f5b5674b0cfbf1191f087ca7dd91059d5eea73565c2ec8c44b827c3f6dfbf985c1be6605e0fbd77350daa7e1b93c666e26c8800dfe23987a90fa8c22799a11d
@@ -0,0 +1,5 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'activesupport', '~> 3.2.0'
4
+ gem 'activemodel', '~> 3.2.0'
5
+ gemspec path: '../'
@@ -0,0 +1,5 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'activesupport', '~> 4.0.0'
4
+ gem 'activemodel', '~> 4.0.0'
5
+ gemspec path: '../'
@@ -0,0 +1,5 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'activesupport', '~> 4.1.0'
4
+ gem 'activemodel', '~> 4.1.0'
5
+ gemspec path: '../'
@@ -0,0 +1,22 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ *.bundle
19
+ *.so
20
+ *.o
21
+ *.a
22
+ mkmf.log
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --color
2
+ --order rand
3
+ --require spec_helper
@@ -0,0 +1,11 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9
4
+ - 2.0
5
+ - 2.1
6
+ - jruby
7
+ - rbx-2
8
+ gemfile:
9
+ - .gemfiles/rails3_2.gemfile
10
+ - .gemfiles/rails4_0.gemfile
11
+ - .gemfiles/rails4_1.gemfile
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in json_validator.gemspec
4
+ gemspec
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Iain Beeston
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,34 @@
1
+ # JsonValidator
2
+
3
+ [![Build Status](http://img.shields.io/travis/iainbeeston/json_validator/master.svg)](https://travis-ci.org/iainbeeston/json_validator)
4
+ [![Code Climate](http://img.shields.io/codeclimate/github/iainbeeston/json_validator.svg)](https://codeclimate.com/github/iainbeeston/json_validator)
5
+
6
+ JsonValidator is an ActiveModel validator that validates any hash field against [JSONSchema](http://json-schema.org), returning errors in the model's own `errors` attribute.
7
+
8
+ This gem was originally written to provide deep validation of JSON attributes, which are available alongside primative types in recent versions of [PostgreSQL](http://www.postgresql.org), but it works equally well with ActiveModel objects.
9
+
10
+ Most of the functionality is dependent on the wonderful [json-schema](https://github.com/hoxworth/json-schema) gem.
11
+
12
+ ## Usage
13
+
14
+ If you're using Ruby on Rails and ActiveRecord, add a validation to your model like this:
15
+
16
+ class Foo < ActiveRecord::Base
17
+ validates :bar, json: {
18
+ schema: {
19
+ '$schema' => 'http://json-schema.org/schema#',
20
+ 'title': 'Universal spoons schema',
21
+ 'properties': {
22
+ 'handleSize': {
23
+ 'type': 'integer',
24
+ 'minimum': 0
25
+ }
26
+ },
27
+ 'required': ['handleSize']
28
+ }
29
+ }
30
+ end
31
+
32
+ Then whenever an instance of `Foo` is saved, `Foo.bar` (assumed to be a hash) will be validated against the JSON schema specified. In this case, `Foo.new(bar: { handleSize: -10 })` would be invalid, but `Foo.new(bar: { handleSize: 10 })` would be valid.
33
+
34
+ The attribute being validated can be either a hash or a string (which will be parsed as JSON). The schema can be either a hash or a Proc that returns a hash (if you'd like to decide on the schema at runtime), and there's no reason why you could not load your schema from a .json file.
@@ -0,0 +1,6 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task default: :spec
@@ -0,0 +1,28 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "json_validator_meta"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "json_validator"
8
+ spec.version = JsonValidatorMeta::VERSION
9
+ spec.authors = ["Iain Beeston"]
10
+ spec.email = ["iain.beeston@gmail.com"]
11
+ spec.summary = %q{ActiveModel that validates hash fields using JSON schema}
12
+ spec.homepage = ""
13
+ spec.license = "MIT"
14
+
15
+ spec.files = `git ls-files -z`.split("\x0")
16
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
17
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
18
+ spec.require_paths = ["lib"]
19
+
20
+ spec.add_dependency "activesupport", ">= 3.2"
21
+ spec.add_dependency "activemodel", ">= 3.2"
22
+ spec.add_dependency "json-schema"
23
+ spec.add_dependency "json"
24
+
25
+ spec.add_development_dependency "bundler", "~> 1.6"
26
+ spec.add_development_dependency "rake"
27
+ spec.add_development_dependency "rspec", ">= 3.0"
28
+ end
@@ -0,0 +1,45 @@
1
+ require 'json_validator_meta'
2
+ # activemodel validators have an undeclared dependency on the hash extensions from active support
3
+ require 'active_support/core_ext/hash'
4
+ require 'active_model/validator'
5
+ require 'json'
6
+ require 'json-schema'
7
+
8
+ class JsonValidator < ActiveModel::EachValidator
9
+ VERSION = JsonValidatorMeta::VERSION
10
+
11
+ def validate_each(record, attribute, value)
12
+ raw_errors = JSON::Validator.fully_validate(schema(record), json(value))
13
+ translated_errors = raw_errors.map do |e|
14
+ translate_message(e)
15
+ end
16
+ translated_errors.each do |e|
17
+ record.errors.add(attribute, e)
18
+ end
19
+ end
20
+
21
+ def schema(record)
22
+ if options[:schema].respond_to?(:call)
23
+ options[:schema].call(record)
24
+ else
25
+ options[:schema].to_hash
26
+ end
27
+ end
28
+
29
+ def json(value)
30
+ if value.respond_to?(:to_hash)
31
+ value.to_hash
32
+ else
33
+ JSON.parse(value.to_s)
34
+ end
35
+ end
36
+
37
+ def translate_message(msg)
38
+ # remove suffix
39
+ msg.gsub!(/ in schema .*$/, '')
40
+ end
41
+
42
+ def check_validity!
43
+ fail ArgumentError, :'Schema unspecified. Please specify :schema as either a Proc or a hash' if options[:schema].nil?
44
+ end
45
+ end
@@ -0,0 +1,4 @@
1
+ # can't put this in a JsonValidator module because ActiveModel expects JsonValidator to be a class
2
+ module JsonValidatorMeta
3
+ VERSION = '0.0.1'
4
+ end
@@ -0,0 +1,35 @@
1
+ require 'spec_helper'
2
+ require 'active_model'
3
+ require 'json_validator'
4
+
5
+ class FakeActiveModel
6
+ include ActiveModel::Validations
7
+
8
+ attr_accessor :json_data
9
+
10
+ def initialize(json_data)
11
+ self.json_data = json_data.to_hash
12
+ end
13
+
14
+ validates :json_data, json: {
15
+ allow_blank: true,
16
+ schema: {
17
+ 'type' => 'object',
18
+ 'required' => ['foo']
19
+ }
20
+ }
21
+ end
22
+
23
+ describe FakeActiveModel do
24
+ it 'sets validation errors using JsonValidator' do
25
+ expect(FakeActiveModel.new(hello: :world)).to_not be_valid
26
+ end
27
+
28
+ it 'does not set validation errors using JsonValidator when the json is valid' do
29
+ expect(FakeActiveModel.new(foo: :bar)).to be_valid
30
+ end
31
+
32
+ it 'does not run JsonValidator when allow_blank is true and the json is an empty hash' do
33
+ expect(FakeActiveModel.new({})).to be_valid
34
+ end
35
+ end
@@ -0,0 +1,162 @@
1
+ require 'spec_helper'
2
+ require 'active_model'
3
+ require 'json_validator'
4
+
5
+ class FakeValidatingModel
6
+ include ActiveModel::Validations
7
+
8
+ attr_accessor :json_data
9
+
10
+ def initialize(json_data)
11
+ self.json_data = json_data.to_hash
12
+ end
13
+ end
14
+
15
+ describe JsonValidator do
16
+ describe '#validate_each' do
17
+ let(:model) { FakeValidatingModel.new(json_data) }
18
+ subject { described_class.new(attributes: [:json_data], schema: json_schema) }
19
+
20
+ context 'when the schema is empty' do
21
+ let(:json_data) do
22
+ {
23
+ 'soup' => 'tastes good'
24
+ }
25
+ end
26
+ subject { described_class.new(attributes: [:json_data], schema: {}) }
27
+
28
+ it 'does not set any errors' do
29
+ expect { subject.validate_each(model, :json_data, json_data) }.to_not change { model.errors.empty? }
30
+ end
31
+ end
32
+
33
+ context 'when a schema specifies required fields' do
34
+ let(:json_data) do
35
+ {
36
+ 'bread' => 'is crusty'
37
+ }
38
+ end
39
+ let(:json_schema) do
40
+ {
41
+ 'type' => 'object',
42
+ 'required' => ['soup']
43
+ }
44
+ end
45
+
46
+ it 'sets errors for required fields that are blank' do
47
+ expect {
48
+ subject.validate_each(model, :json_data, json_data)
49
+ }.to change {
50
+ model.errors.to_hash
51
+ }.from(
52
+ {}
53
+ ).to(
54
+ {
55
+ json_data: ["The property '#/' did not contain a required property of 'soup'"]
56
+ }
57
+ )
58
+ end
59
+ end
60
+
61
+ context 'when a schema specifies minLength' do
62
+ let(:json_data) do
63
+ {
64
+ 'menu' => 'does not have enough variety',
65
+ 'address' => 'is fine'
66
+ }
67
+ end
68
+ let(:json_schema) do
69
+ {
70
+ 'type' => 'object',
71
+ 'properties' => {
72
+ 'menu' => {
73
+ 'type' => 'string',
74
+ 'minLength' => 200
75
+ },
76
+ 'address' => {
77
+ 'type' => 'string',
78
+ 'minLength' => 3
79
+ }
80
+ }
81
+ }
82
+ end
83
+
84
+ it 'sets errors for fields with minLength that do not meet the required length' do
85
+ expect {
86
+ subject.validate_each(model, :json_data, json_data)
87
+ }.to change {
88
+ model.errors.to_hash
89
+ }.from(
90
+ {}
91
+ ).to(
92
+ {
93
+ json_data: ["The property '#/menu' was not of a minimum string length of 200"]
94
+ }
95
+ )
96
+ end
97
+ end
98
+ end
99
+
100
+ describe '#initialize' do
101
+ context 'when a schema is not specified' do
102
+ it 'raises an error' do
103
+ expect {
104
+ described_class.new(attributes: [:name])
105
+ }.to raise_error(ArgumentError).with_message(
106
+ 'Schema unspecified. Please specify :schema as either a Proc or a hash'
107
+ )
108
+ end
109
+ end
110
+ end
111
+
112
+ describe '#schema' do
113
+ context 'when initialized with a :schema option' do
114
+ context 'that is a lambda' do
115
+ subject { described_class.new(attributes: [:name], schema: ->(model) { { foo: model.name } }) }
116
+
117
+ it 'is the result of the lambda (passed the model)' do
118
+ expect(subject.schema(double(name: 'ack'))).to eq(foo: 'ack')
119
+ end
120
+ end
121
+
122
+ context 'that is a hash' do
123
+ subject { described_class.new(attributes: [:name], schema: { foo: :bar }) }
124
+
125
+ it 'is the hash' do
126
+ expect(subject.schema(double)).to eq(foo: :bar)
127
+ end
128
+ end
129
+ end
130
+ end
131
+
132
+ describe '#json' do
133
+ subject { described_class.new(attributes: [:name], schema: {}) }
134
+
135
+ it 'returns the input when passed a hash' do
136
+ expect(subject.json('foo' => 'bar')).to eq('foo' => 'bar')
137
+ end
138
+
139
+ it 'parses the input as JSON when passed a string' do
140
+ expect(subject.json('{"foo": "bar"}')).to eq('foo' => 'bar')
141
+ end
142
+
143
+ it 'calls #to_hash when passed any other object' do
144
+ expect(subject.json(double(to_hash: { 'foo' => 'bar' }))).to eq('foo' => 'bar')
145
+ end
146
+ end
147
+
148
+ describe '#translate_message' do
149
+ subject { described_class.new(attributes: [:name], schema: { foo: :bar }) }
150
+
151
+ it 'translates json-schema messages to slightly more readable ones' do
152
+ msg = "The property '#/menu' was not of a minimum string length of 200 in schema 40148e2f-45d6-51b7-972a-179bd9de61d6#"
153
+ expect(subject.translate_message(msg)).to eq("The property '#/menu' was not of a minimum string length of 200")
154
+ end
155
+ end
156
+
157
+ describe '#VERSION' do
158
+ it 'is the same as JsonValidatorMeta::VERSION' do
159
+ expect(described_class.const_get(:VERSION)).to eq(JsonValidatorMeta::VERSION)
160
+ end
161
+ end
162
+ end
@@ -0,0 +1,57 @@
1
+ # This file was generated by the `rspec --init` command. Conventionally, all
2
+ # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
3
+ # The generated `.rspec` file contains `--require spec_helper` which will cause this
4
+ # file to always be loaded, without a need to explicitly require it in any files.
5
+ #
6
+ # Given that it is always loaded, you are encouraged to keep this file as
7
+ # light-weight as possible. Requiring heavyweight dependencies from this file
8
+ # will add to the boot time of your test suite on EVERY test run, even for an
9
+ # individual file that may not need all of that loaded. Instead, make a
10
+ # separate helper file that requires this one and then use it only in the specs
11
+ # that actually need it.
12
+ #
13
+ # The `.rspec` file also contains a few flags that are not defaults but that
14
+ # users commonly want.
15
+ #
16
+ # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
17
+ RSpec.configure do |config|
18
+ # These two settings work together to allow you to limit a spec run
19
+ # to individual examples or groups you care about by tagging them with
20
+ # `:focus` metadata. When nothing is tagged with `:focus`, all examples
21
+ # get run.
22
+ config.filter_run :focus
23
+ config.run_all_when_everything_filtered = true
24
+
25
+ # Many RSpec users commonly either run the entire suite or an individual
26
+ # file, and it's useful to allow more verbose output when running an
27
+ # individual spec file.
28
+ if config.files_to_run.one?
29
+ # Use the documentation formatter for detailed output,
30
+ # unless a formatter has already been configured
31
+ # (e.g. via a command-line flag).
32
+ config.default_formatter = 'doc'
33
+ end
34
+
35
+ # rspec-expectations config goes here. You can use an alternate
36
+ # assertion/expectation library such as wrong or the stdlib/minitest
37
+ # assertions if you prefer.
38
+ config.expect_with :rspec do |expectations|
39
+ # Enable only the newer, non-monkey-patching expect syntax.
40
+ # For more details, see:
41
+ # - http://myronmars.to/n/dev-blog/2012/06/rspecs-new-expectation-syntax
42
+ expectations.syntax = :expect
43
+ end
44
+
45
+ # rspec-mocks config goes here. You can use an alternate test double
46
+ # library (such as bogus or mocha) by changing the `mock_with` option here.
47
+ config.mock_with :rspec do |mocks|
48
+ # Enable only the newer, non-monkey-patching expect syntax.
49
+ # For more details, see:
50
+ # - http://teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/
51
+ mocks.syntax = :expect
52
+
53
+ # Prevents you from mocking or stubbing a method that does not exist on
54
+ # a real object. This is generally recommended.
55
+ mocks.verify_partial_doubles = true
56
+ end
57
+ end
metadata ADDED
@@ -0,0 +1,161 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: json_validator
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Iain Beeston
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-07-14 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '3.2'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '3.2'
27
+ - !ruby/object:Gem::Dependency
28
+ name: activemodel
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '3.2'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '3.2'
41
+ - !ruby/object:Gem::Dependency
42
+ name: json-schema
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: json
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: bundler
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '1.6'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '1.6'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rake
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rspec
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '3.0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '3.0'
111
+ description:
112
+ email:
113
+ - iain.beeston@gmail.com
114
+ executables: []
115
+ extensions: []
116
+ extra_rdoc_files: []
117
+ files:
118
+ - ".gemfiles/rails3_2.gemfile"
119
+ - ".gemfiles/rails4_0.gemfile"
120
+ - ".gemfiles/rails4_1.gemfile"
121
+ - ".gitignore"
122
+ - ".rspec"
123
+ - ".travis.yml"
124
+ - Gemfile
125
+ - LICENSE.txt
126
+ - README.md
127
+ - Rakefile
128
+ - json_validator.gemspec
129
+ - lib/json_validator.rb
130
+ - lib/json_validator_meta.rb
131
+ - spec/integration/active_model_spec.rb
132
+ - spec/lib/json_validator_spec.rb
133
+ - spec/spec_helper.rb
134
+ homepage: ''
135
+ licenses:
136
+ - MIT
137
+ metadata: {}
138
+ post_install_message:
139
+ rdoc_options: []
140
+ require_paths:
141
+ - lib
142
+ required_ruby_version: !ruby/object:Gem::Requirement
143
+ requirements:
144
+ - - ">="
145
+ - !ruby/object:Gem::Version
146
+ version: '0'
147
+ required_rubygems_version: !ruby/object:Gem::Requirement
148
+ requirements:
149
+ - - ">="
150
+ - !ruby/object:Gem::Version
151
+ version: '0'
152
+ requirements: []
153
+ rubyforge_project:
154
+ rubygems_version: 2.2.2
155
+ signing_key:
156
+ specification_version: 4
157
+ summary: ActiveModel that validates hash fields using JSON schema
158
+ test_files:
159
+ - spec/integration/active_model_spec.rb
160
+ - spec/lib/json_validator_spec.rb
161
+ - spec/spec_helper.rb