strong_json 0.0.1

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,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