strong_json 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: 87d564cb26c5d9ed754f678b8457500a559bdc59
4
+ data.tar.gz: 9295697d16ad7e6bae591d2fba87c8faac18032e
5
+ SHA512:
6
+ metadata.gz: af91e4213775d941fdaba00e8eaedc6b00794505b02ae9d1b1f43d9d1ae0e52931adc7a1a7b5c041052a6bcd09bd409c6397e6257734ef6006a634f3c9daeaf6
7
+ data.tar.gz: 223485b1b051ee4651840797e064607160858b25223472a5b4fafc1be063ca329ab343b9d9224fc7d2306cf1d38ae1cf32aa28b63252c59eb603aae541ba7865
@@ -0,0 +1,14 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.bundle
11
+ *.so
12
+ *.o
13
+ *.a
14
+ mkmf.log
@@ -0,0 +1,5 @@
1
+ language: ruby
2
+ rvm:
3
+ - "2.1.1"
4
+ - "2.1.2"
5
+ - "2.1.3"
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in strong_json.gemspec
4
+ gemspec
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Soutaro Matsumoto
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,76 @@
1
+ # StrongJSON
2
+
3
+ This library allows you to test the structure of JSON objects.
4
+
5
+ This is similar to Strong Parameters, which is introduced by Rails 4, but expected to work with more complex structures.
6
+ It may help you to understand what this is as: Strong Parameter is for simple structures, like HTML forms, and StrongJSON is for complex structures, like JSON objects posted to API.
7
+
8
+ ## Installation
9
+
10
+ Add this line to your application's Gemfile:
11
+
12
+ ```ruby
13
+ gem 'strong_json'
14
+ ```
15
+
16
+ And then execute:
17
+
18
+ $ bundle
19
+
20
+ Or install it yourself as:
21
+
22
+ $ gem install strong_json
23
+
24
+ ## Usage
25
+
26
+ ```ruby
27
+ s = StrongJSON.new do
28
+ let :item, object(id: prohibited, name: string, count: numeric)
29
+ let :custoemr, object(name: string, address: string, phone: string, email: optional(string))
30
+ let :order, object(customer: customer, items: array(item))
31
+ end
32
+
33
+ json = s.order.coerce(JSON.parse(input))
34
+ ```
35
+
36
+ If the input JSON data is conformant with `order`'s structure, the `json` will be that value.
37
+
38
+ If the input JSON contains attributes which is not white-listed in the definition, the fields will be droped.
39
+
40
+ If an attribute has a value which does not match with given type, the `coerce` method call will raise an exception `StrongJSON::Type::Error`.
41
+ If the input JSON contains `prohibited` attributes, `id` of `item` in the example, it also will result in an exception.
42
+
43
+ ## Catalogue of Types
44
+
45
+ ### object(f1: type1, f2: type2, ...)
46
+
47
+ * The value must be an object
48
+ * Fields, `f1`, `f2`, and ..., must be present and its values must be of `type1`, `type2`, ..., respectively
49
+ * Other fields will be ignored
50
+
51
+ ### array(type)
52
+
53
+ * The value must be an array
54
+ * All elements in the array must be value of given `type`
55
+
56
+ ### optional(type)
57
+
58
+ * The value can be `nil` (or not contained in an object)
59
+ * If an value exists, it must be of given `type`
60
+
61
+ ### Base types
62
+
63
+ * `number` The value must be an instance of `Numeric`
64
+ * `string` The value must be an instance of `String`
65
+ * `boolean` The value must be `true` or `false`
66
+ * `numeric` The value must be an instance of `Numeric` or a string which represents a number
67
+ * `any` Any value except `nil` is accepted
68
+ * `prohibited` Any value will be rejected
69
+
70
+ ## Contributing
71
+
72
+ 1. Fork it ( https://github.com/soutaro/strong_json/fork )
73
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
74
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
75
+ 4. Push to the branch (`git push origin my-new-feature`)
76
+ 5. Create a new Pull Request
@@ -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,15 @@
1
+ require "strong_json/version"
2
+ require "strong_json/type"
3
+ require "strong_json/types"
4
+
5
+ class StrongJSON
6
+ def initialize(&block)
7
+ instance_eval(&block)
8
+ end
9
+
10
+ def let(name, type)
11
+ define_method(name) { type }
12
+ end
13
+
14
+ include StrongJSON::Types
15
+ end
@@ -0,0 +1,128 @@
1
+ class StrongJSON
2
+ module Type
3
+ class Base
4
+ def initialize(type)
5
+ @type = type
6
+ end
7
+
8
+ def test(value)
9
+ case @type
10
+ when :prohibited
11
+ false
12
+ when :any
13
+ true
14
+ when :number
15
+ value.is_a?(Numeric)
16
+ when :string
17
+ value.is_a?(String)
18
+ when :boolean
19
+ value == true || value == false
20
+ when :numeric
21
+ value.is_a?(Numeric) || value.is_a?(String) && /\A[\d.]+\Z/ =~ value
22
+ else
23
+ false
24
+ end
25
+ end
26
+
27
+ def coerce(value, path: [])
28
+ raise Error.new(value: value, type: self, path: path) unless test(value)
29
+ value
30
+ end
31
+
32
+ def to_s
33
+ @type.to_s
34
+ end
35
+ end
36
+
37
+ class Optional
38
+ def initialize(type)
39
+ @type = type
40
+ end
41
+
42
+ def coerce(value, path: [])
43
+ if value != nil
44
+ @type.coerce(value, path: path)
45
+ else
46
+ nil
47
+ end
48
+ end
49
+
50
+ def to_s
51
+ "optinal(#{@type})"
52
+ end
53
+ end
54
+
55
+ class Array
56
+ def initialize(type)
57
+ @type = type
58
+ end
59
+
60
+ def coerce(value, path: [])
61
+ if value.is_a?(::Array)
62
+ value.map.with_index do |v, i|
63
+ @type.coerce(v, path: path+[i])
64
+ end
65
+ else
66
+ raise Error.new(path: path, type: self, value: value)
67
+ end
68
+ end
69
+
70
+ def to_s
71
+ "array(#{@type})"
72
+ end
73
+ end
74
+
75
+ class Object
76
+ def initialize(fields)
77
+ @fields = fields
78
+ end
79
+
80
+ def coerce(object, path: [])
81
+ unless object.is_a?(Hash)
82
+ raise Error.new(path: path, type: self, value: object)
83
+ end
84
+
85
+ result = {}
86
+
87
+ @fields.each do |name, ty|
88
+ value = ty.coerce(object[name], path: path + [name])
89
+ result[name] = value if object.has_key?(name)
90
+ end
91
+
92
+ result
93
+ end
94
+
95
+ def merge(fields)
96
+ if fields.is_a?(Object)
97
+ fields = Object.instance_variable_get("@fields")
98
+ end
99
+
100
+ Object.new(@fields.merge(fields))
101
+ end
102
+
103
+ def to_s
104
+ fields = []
105
+
106
+ @fields.each do |name, type|
107
+ fields << "#{name}: #{type}"
108
+ end
109
+
110
+ "object(#{fields.join(', ')})"
111
+ end
112
+ end
113
+
114
+ class Error < StandardError
115
+ attr_reader :path, :type, :value
116
+
117
+ def initialize(path:, type:, value:)
118
+ @path = path
119
+ @type = type
120
+ @value = value
121
+ end
122
+
123
+ def to_s
124
+ "Expected type of value at #{path.join('.')} (#{value}) is #{type}"
125
+ end
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,39 @@
1
+ class StrongJSON
2
+ module Types
3
+ def object(fields = {})
4
+ Type::Object.new(fields)
5
+ end
6
+
7
+ def array(type = any)
8
+ Type::Array.new(type)
9
+ end
10
+
11
+ def optional(type = any)
12
+ Type::Optional.new(type)
13
+ end
14
+
15
+ def string
16
+ StrongJSON::Type::Base.new(:string)
17
+ end
18
+
19
+ def numeric
20
+ StrongJSON::Type::Base.new(:numeric)
21
+ end
22
+
23
+ def number
24
+ StrongJSON::Type::Base.new(:number)
25
+ end
26
+
27
+ def boolean
28
+ StrongJSON::Type::Base.new(:boolean)
29
+ end
30
+
31
+ def any
32
+ StrongJSON::Type::Base.new(:any)
33
+ end
34
+
35
+ def prohibited
36
+ StrongJSON::Type::Base.new(:prohibited)
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,3 @@
1
+ class StrongJSON
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,38 @@
1
+ require "strong_json"
2
+
3
+ describe StrongJSON::Type::Array, "#coerce" do
4
+ it "returns empty" do
5
+ type = StrongJSON::Type::Array.new(StrongJSON::Type::Base.new(:any))
6
+
7
+ expect(type.coerce([])).to eq([])
8
+ end
9
+
10
+ it "test array of number" do
11
+ type = StrongJSON::Type::Array.new(StrongJSON::Type::Base.new(:number))
12
+ expect(type.coerce([1])).to eq([1])
13
+ end
14
+
15
+ it "test array of array of number" do
16
+ a = StrongJSON::Type::Array.new(StrongJSON::Type::Base.new(:number))
17
+ type = StrongJSON::Type::Array.new(a)
18
+ expect(type.coerce([[1]])).to eq([[1]])
19
+ end
20
+
21
+ it "reject non array" do
22
+ type = StrongJSON::Type::Array.new(StrongJSON::Type::Base.new(:number))
23
+
24
+ expect { type.coerce({}) }.to raise_error(StrongJSON::Type::Error)
25
+ end
26
+
27
+ it "reject membership" do
28
+ type = StrongJSON::Type::Array.new(StrongJSON::Type::Base.new(:number))
29
+
30
+ expect { type.coerce(["a"]) }.to raise_error(StrongJSON::Type::Error)
31
+ end
32
+
33
+ it "rejects nil" do
34
+ type = StrongJSON::Type::Array.new(StrongJSON::Type::Base.new(:number))
35
+
36
+ expect { type.coerce(nil) }.to raise_error(StrongJSON::Type::Error)
37
+ end
38
+ end
@@ -0,0 +1,85 @@
1
+ require "strong_json"
2
+
3
+ describe StrongJSON::Type::Base do
4
+ describe "#test" do
5
+ context ":prohibited" do
6
+ let (:type) { StrongJSON::Type::Base.new(:prohibited) }
7
+
8
+ it "rejects number" do
9
+ expect(type.test(123)).to be_falsey
10
+ end
11
+ end
12
+
13
+ context ":number" do
14
+ let (:type) { StrongJSON::Type::Base.new(:number) }
15
+
16
+ it "accepts integer" do
17
+ expect(type.test(123)).to be_truthy
18
+ end
19
+
20
+ it "accepts float" do
21
+ expect(type.test(3.14)).to be_truthy
22
+ end
23
+
24
+ it "rejects string" do
25
+ expect(type.test("string")).to be_falsey
26
+ end
27
+ end
28
+
29
+ context ":string" do
30
+ let (:type) { StrongJSON::Type::Base.new(:string) }
31
+
32
+ it "accepts string" do
33
+ expect(type.test("string")).to be_truthy
34
+ end
35
+ end
36
+
37
+ context ":any" do
38
+ let (:type) { StrongJSON::Type::Base.new(:any) }
39
+
40
+ it "accepts string" do
41
+ expect(type.test("string")).to be_truthy
42
+ end
43
+
44
+ it "accepts number" do
45
+ expect(type.test(2.71828)).to be_truthy
46
+ end
47
+ end
48
+
49
+ context ":boolean" do
50
+ let (:type) { StrongJSON::Type::Base.new(:boolean) }
51
+
52
+ it "accepts true" do
53
+ expect(type.test(true)).to be_truthy
54
+ end
55
+
56
+ it "accepts false" do
57
+ expect(type.test(false)).to be_truthy
58
+ end
59
+
60
+ it "rejects nil" do
61
+ expect(type.test(nil)).to be_falsey
62
+ end
63
+ end
64
+
65
+ context ":numeric" do
66
+ let (:type) { StrongJSON::Type::Base.new(:numeric) }
67
+
68
+ it "accepts number" do
69
+ expect(type.test(123)).to be_truthy
70
+ end
71
+
72
+ it "accepts number format string" do
73
+ expect(type.test("123")).to be_truthy
74
+ end
75
+
76
+ it "rejects non numeric format string" do
77
+ expect(type.test("test")).to be_falsey
78
+ end
79
+
80
+ it "rejects boolean" do
81
+ expect(type.test(true)).to be_falsey
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,8 @@
1
+ describe StrongJSON::Type::Error do
2
+ include StrongJSON::Types
3
+
4
+ it "hgoehoge" do
5
+ exn = StrongJSON::Type::Error.new(value: [], type: array(numeric), path: ["a",1,"b"])
6
+ expect(exn.to_s).to be_a(String)
7
+ end
8
+ end
@@ -0,0 +1,52 @@
1
+ require "strong_json"
2
+
3
+ describe StrongJSON::Type::Object do
4
+ describe "#coerce" do
5
+ it "accepts value" do
6
+ type = StrongJSON::Type::Object.new(a: StrongJSON::Type::Base.new(:numeric),
7
+ b: StrongJSON::Type::Base.new(:string))
8
+
9
+ expect(type.coerce(a: 123, b: "test")).to eq(a: 123, b: "test")
10
+ end
11
+
12
+ it "drops unspecified fields" do
13
+ type = StrongJSON::Type::Object.new(a: StrongJSON::Type::Base.new(:numeric))
14
+
15
+ expect(type.coerce(a: 123, b: true)).to eq(a: 123)
16
+ end
17
+
18
+ it "rejects prohibited fields" do
19
+ type = StrongJSON::Type::Object.new(a: StrongJSON::Type::Base.new(:prohibited))
20
+
21
+ expect{ type.coerce(a: 123, b: true) }.to raise_error(StrongJSON::Type::Error)
22
+ end
23
+
24
+ it "rejects objects with missing fields" do
25
+ type = StrongJSON::Type::Object.new(a: StrongJSON::Type::Base.new(:numeric))
26
+
27
+ expect{ type.coerce(b: "test") }.to raise_error(StrongJSON::Type::Error)
28
+ end
29
+
30
+ it "accepts missing field if optional" do
31
+ type = StrongJSON::Type::Object.new(a: StrongJSON::Type::Optional.new(StrongJSON::Type::Base.new(:numeric)))
32
+
33
+ expect(type.coerce(b: "test")).to eq({})
34
+ end
35
+ end
36
+
37
+ describe "#merge" do
38
+ let (:type) { StrongJSON::Type::Object.new(a: StrongJSON::Type::Base.new(:numeric)) }
39
+
40
+ it "adds field" do
41
+ ty2 = type.merge(b: StrongJSON::Type::Base.new(:string))
42
+
43
+ expect(ty2.coerce(a: 123, b: "test")).to eq(a: 123, b: "test")
44
+ end
45
+
46
+ it "overrides field" do
47
+ ty2 = type.merge(a: StrongJSON::Type::Base.new(:prohibited))
48
+
49
+ expect{ ty2.coerce(a: 123) }.to raise_error(StrongJSON::Type::Error)
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,19 @@
1
+ require "strong_json"
2
+
3
+ describe StrongJSON::Type::Optional, "#coerce" do
4
+ context "optional(:number)" do
5
+ let (:type) { StrongJSON::Type::Optional.new(StrongJSON::Type::Base.new(:number)) }
6
+
7
+ it "accepts nil" do
8
+ expect(type.coerce(nil)).to eq(nil)
9
+ end
10
+
11
+ it "accepts number" do
12
+ expect(type.coerce(3)).to eq(3)
13
+ end
14
+
15
+ it "rejects string" do
16
+ expect { type.coerce("a") }.to raise_error(StrongJSON::Type::Error)
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,10 @@
1
+ describe "StrongJSON.new" do
2
+ it "tests the structure of a JSON object" do
3
+ s = StrongJSON.new do
4
+ let :item, object(id: prohibited, name: string, count: numeric, price: numeric)
5
+ let :checkout, object(id: prohibited, items: array(item), change: optional(number))
6
+ end
7
+
8
+ expect(s.checkout.coerce(items: [ { name: "test", count: 1, price: "2.33", comment: "dummy" } ])).to eq(items: [ { name: "test", count: 1, price: "2.33" }])
9
+ end
10
+ end
@@ -0,0 +1,24 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'strong_json/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "strong_json"
8
+ spec.version = StrongJSON::VERSION
9
+ spec.authors = ["Soutaro Matsumoto"]
10
+ spec.email = ["matsumoto@soutaro.com"]
11
+ spec.summary = "Type check JSON objects"
12
+ spec.description = "Type check JSON objects"
13
+ spec.homepage = ""
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler", "~> 1.6"
22
+ spec.add_development_dependency "rake", "~> 10.0"
23
+ spec.add_development_dependency "rspec", "~> 3.0"
24
+ end
metadata ADDED
@@ -0,0 +1,109 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: strong_json
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Soutaro Matsumoto
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-10-11 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.6'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.6'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.0'
55
+ description: Type check JSON objects
56
+ email:
57
+ - matsumoto@soutaro.com
58
+ executables: []
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - ".gitignore"
63
+ - ".travis.yml"
64
+ - Gemfile
65
+ - LICENSE.txt
66
+ - README.md
67
+ - Rakefile
68
+ - lib/strong_json.rb
69
+ - lib/strong_json/type.rb
70
+ - lib/strong_json/types.rb
71
+ - lib/strong_json/version.rb
72
+ - spec/array_spec.rb
73
+ - spec/basetype_spec.rb
74
+ - spec/error_spec.rb
75
+ - spec/object_spec.rb
76
+ - spec/optional_spec.rb
77
+ - spec/spec.rb
78
+ - strong_json.gemspec
79
+ homepage: ''
80
+ licenses:
81
+ - MIT
82
+ metadata: {}
83
+ post_install_message:
84
+ rdoc_options: []
85
+ require_paths:
86
+ - lib
87
+ required_ruby_version: !ruby/object:Gem::Requirement
88
+ requirements:
89
+ - - ">="
90
+ - !ruby/object:Gem::Version
91
+ version: '0'
92
+ required_rubygems_version: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ requirements: []
98
+ rubyforge_project:
99
+ rubygems_version: 2.2.2
100
+ signing_key:
101
+ specification_version: 4
102
+ summary: Type check JSON objects
103
+ test_files:
104
+ - spec/array_spec.rb
105
+ - spec/basetype_spec.rb
106
+ - spec/error_spec.rb
107
+ - spec/object_spec.rb
108
+ - spec/optional_spec.rb
109
+ - spec/spec.rb