hash_control 0.1.2

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 6c58ba9b45b5b93680cc8d22d8dd859b355bafd3
4
+ data.tar.gz: c98ed21ba6469980f703b23f2eefeccdd6f2f8f0
5
+ SHA512:
6
+ metadata.gz: c0bf551ed0f381f37b44fc87d922ca5aec7bd16f7d8839388e58c557528f69110ea962125e991608fd35a0d9abb4ca149426a755c9cee5b13df6c7f9c8188504
7
+ data.tar.gz: e72d077a10b5161b67637ad9602df132cb753283db5b5bf6d006f172783f538ac8f6c2b56f24c06496cd2f437bd7fd6b175ec21f7e01e98e372cc17d137c69f8
data/.gitignore ADDED
@@ -0,0 +1,2 @@
1
+ /Gemfile.lock
2
+ /hash_control-*.gem
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --color
2
+ --format=documentation
3
+ --tty
data/.travis.yml ADDED
@@ -0,0 +1,7 @@
1
+ language: ruby
2
+ script: rspec
3
+ rvm:
4
+ - 1.9.3
5
+ - 2.0.0
6
+ - 2.1.0
7
+ - ruby-head
data/Gemfile ADDED
@@ -0,0 +1,7 @@
1
+ source "http://rubygems.org"
2
+
3
+ gem 'activesupport', '~> 4.0'
4
+
5
+ group :test do
6
+ gem 'rspec', '~> 3.0'
7
+ end
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2014-2015 Sean Zhu
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Makefile ADDED
@@ -0,0 +1,15 @@
1
+ GEM_NAME := hash_control
2
+
3
+ all:
4
+ gem build $(GEM_NAME).gemspec
5
+
6
+ install: all
7
+ gem install $(GEM_NAME)
8
+
9
+ uninstall:
10
+ gem uninstall $(GEM_NAME)
11
+
12
+ clean:
13
+ rm -fv -- $(GEM_NAME)-*.gem
14
+
15
+ .PHONY: all install clean
data/README.md ADDED
@@ -0,0 +1,91 @@
1
+ # HashControl
2
+
3
+ [![Build Status](https://travis-ci.org/szhu/hashcontrol.svg?branch=master)](https://travis-ci.org/szhu/hashcontrol)
4
+ [![Code Climate](https://codeclimate.com/github/szhu/hashcontrol/badges/gpa.svg)](https://codeclimate.com/github/szhu/hashcontrol)
5
+
6
+ ```shell
7
+ gem install hash_control
8
+ ```
9
+
10
+ This Ruby library provides some conveniences for using and manipulating hash-like data.
11
+
12
+ ## Features
13
+
14
+ `HashControl::Model` is a class with
15
+
16
+ - validation checking
17
+ - getting and setting properties with both `[]` and accessor methods
18
+ - setter methods can be omitted to prevent mutation
19
+ - [AwesomePrint](https://github.com/michaeldv/awesome_print) support
20
+
21
+ Want just a single-use validator? Use `HashControl::Validator`.
22
+
23
+ ## Examples
24
+
25
+ ### Model
26
+
27
+ ```ruby
28
+ require 'hash_control'
29
+ class Comment
30
+ include ::HashControl::Model
31
+ require_key :author, :body, :date
32
+ permit_key :image
33
+ end
34
+
35
+ require 'hash_control'
36
+ class Something
37
+ include ::HashControl::Model
38
+ require_key :id
39
+ permit_all_keys
40
+ end
41
+
42
+ Comment.new(author: 'me', body: 'interesting stuff', date: Time.now)
43
+
44
+ Comment.new(body: "this ain't gonna fly")
45
+ # ArgumentError: extra params [:extra]
46
+ # in {:body=>"this ain't gonna fly", :author=>"me", :date=>2014-01-01 00:00:00 -0000, :extra=>"hullo"}
47
+
48
+ Something.new(body: "this, however, will")
49
+ # ArgumentError: required params [:id] missing
50
+ # in {:body=>"this, however, will"}
51
+
52
+ Something.new(id: 1, body: "oops my bad")
53
+ ```
54
+
55
+ ### Validator
56
+
57
+ ```ruby
58
+ require 'hash_control'
59
+ get_request = {
60
+ get: '/api/article/23/comment/34/show'
61
+ }
62
+ post_request = {
63
+ post: '/api/article/23/comment/34/update',
64
+ body: {
65
+ date: Time.new,
66
+ body: 'hullo',
67
+ meta: 'fsdkfsifhdsfsdhkj'
68
+ }
69
+ }
70
+ validator = ::HashControl::Validator.new(post_request)
71
+ validator.require(:post).permit(:body).only
72
+ # `permit` marks keys as allowed but doesn't do any verification
73
+ # `only` ensures no other keys are present
74
+
75
+ class CustomValidator < ::HashControl::Validator
76
+ def validate_request
77
+ require_one_of(:get, :post)
78
+ end
79
+
80
+ def validate_get_request
81
+ validate_request.only
82
+ end
83
+
84
+ def validate_post_request
85
+ validate_request.permit(:body).only
86
+ end
87
+ end
88
+
89
+ CustomValidator.new(get_request).validate_get_request
90
+ CustomValidator.new(post_request).validate_post_request
91
+ ```
@@ -0,0 +1,21 @@
1
+ require './lib/hash_control/version'
2
+
3
+ Gem::Specification.new do |gem|
4
+ gem.name = File.basename(ARGV[1], '.gemspec')
5
+ gem.summary = 'Conveniently validating and manipulating hash-like data.'
6
+ gem.description = <<-END
7
+ Provides some conveniences for validating and manipulating hash-like data.
8
+ END
9
+
10
+ gem.version = ::HashControl::VERSION
11
+ gem.date = '2014-08-22'
12
+
13
+ gem.homepage = 'https://github.com/szhu/hashcontrol'
14
+ gem.authors = ['Sean Zhu']
15
+ gem.email = 'interestinglythere@gmail.com'
16
+ gem.license = 'MIT'
17
+
18
+ gem.add_dependency 'activesupport', '~> 4.0'
19
+ gem.files = `git ls-files`.split($RS)
20
+ gem.require_paths = ['lib']
21
+ end
@@ -0,0 +1,126 @@
1
+ require 'active_support/core_ext/hash/indifferent_access'
2
+ require 'active_support/hash_with_indifferent_access'
3
+ require_relative 'validator'
4
+
5
+ module HashControl
6
+ # An hash-like type that requires certain keys to be defined
7
+ module Model
8
+ def initialize(hash = {})
9
+ @hash = ::ActiveSupport::HashWithIndifferentAccess.new(hash)
10
+ initialize_model if self.class.method_defined? :initialize_model
11
+ hash_validator = validate_default
12
+ validate(hash_validator) if self.class.method_defined? :validate
13
+ end
14
+
15
+ def symbolized_hash
16
+ @hash.symbolize_keys
17
+ end
18
+
19
+ def slice(*keys)
20
+ @symbolized_hash.select { |key, _| keys.include? key.to_sym }
21
+ end
22
+
23
+ def [](name)
24
+ @hash[name]
25
+ end
26
+
27
+ private
28
+
29
+ def validate_default
30
+ hash_validator = Validator.new(@hash)
31
+ hash_validator.require(*self.class.required_keys)
32
+ unless self.class.permitted_keys == :all
33
+ hash_validator.permit(*self.class.permitted_keys).only
34
+ end
35
+ hash_validator
36
+ end
37
+
38
+ module ClassMethods
39
+ def initialize_class
40
+ instance_variable_set(:@required_keys, Set.new)
41
+ instance_variable_set(:@permitted_keys, Set.new)
42
+ end
43
+ attr_reader :required_keys, :permitted_keys
44
+
45
+ def permit_all_keys
46
+ @permitted_keys = :all
47
+ end
48
+
49
+ def require_key(*keys)
50
+ keys.each { |key| key_accessor(key) }
51
+ required_keys.merge keys
52
+ end
53
+
54
+ def permit_key(*keys)
55
+ keys.each { |key| key_accessor(key) }
56
+ permitted_keys.merge keys
57
+ end
58
+
59
+ def key_accessor(name)
60
+ name = name.to_sym
61
+ return if self.respond_to?(name)
62
+ class_eval do
63
+ define_method(name) { @hash[name] }
64
+ end
65
+ end
66
+ end
67
+
68
+ def self.included(base)
69
+ base.extend ClassMethods
70
+ base.initialize_class
71
+ end
72
+ end
73
+
74
+ # Allow setting
75
+ module WritableModel
76
+ include Model
77
+
78
+ module ClassMethods
79
+ def key_accessor(name)
80
+ name = name.to_sym
81
+ return if self.respond_to?(name)
82
+ class_eval do
83
+ define_method(name) { @hash[name] }
84
+ define_method("#{name}=") { |x| @hash[name] = x }
85
+ end
86
+ end
87
+ end
88
+
89
+ def []=(name, x)
90
+ @hash[name] = x
91
+ end
92
+
93
+ def self.included(base)
94
+ Model.included(base)
95
+ base.extend ClassMethods
96
+ end
97
+ end
98
+
99
+ # Add extra I/O
100
+ # Placed in a separate block for sake of clarity
101
+ module Model
102
+ # JSON support
103
+ module ClassMethods
104
+ def json_create(hash_as_json)
105
+ require 'json'
106
+ hash = JSON.parse hash_as_json
107
+ new hash
108
+ end
109
+ end
110
+
111
+ def to_json
112
+ require 'json'
113
+ @hash.to_json
114
+ end
115
+
116
+ def as_json(_opts = {})
117
+ symbolized_hash
118
+ end
119
+
120
+ # AwesomePrint support
121
+ def ai(options)
122
+ require 'awesome_print'
123
+ AwesomePrint::Inspector.new(options).awesome(symbolized_hash)
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,115 @@
1
+ require 'active_support/hash_with_indifferent_access'
2
+ require 'active_support/inflector'
3
+ require 'set'
4
+
5
+ module HashControl
6
+ class Validator
7
+ def initialize(hash, opts = {})
8
+ if hash.is_a? ::ActiveSupport::HashWithIndifferentAccess
9
+ @hash = hash
10
+ else
11
+ @hash = ::ActiveSupport::HashWithIndifferentAccess.new(hash)
12
+ end
13
+ @error_class = opts[:raising] || ArgumentError
14
+ @term = opts[:term] || 'param'
15
+ @string_keys = opts[:string_keys] || false
16
+ @permitted_keys = Set.new
17
+ end
18
+
19
+ # Specifies keys that must exist
20
+ def require(*keys)
21
+ permitted_keys.merge keys
22
+ required_keys = keys.to_set
23
+ unless (missing_keys = required_keys - hash_keys).empty?
24
+ error "required #{terms} #{missing_keys.to_a} missing" + postscript
25
+ end
26
+ self
27
+ end
28
+
29
+ def require_n_of(n, *keys)
30
+ permitted_keys.merge keys
31
+ required_keys = keys.to_set
32
+ if (missing_keys = required_keys - hash_keys).length > n
33
+ error "#{n} or more #{terms} in #{missing_keys.to_a} must be given" + postscript
34
+ end
35
+ self
36
+ end
37
+
38
+ def require_one_of(*keys)
39
+ require_n_of(1, *keys)
40
+ end
41
+
42
+ # Specifies keys that can exist with no further restrictions
43
+ # Does no checking on its own
44
+ def permit(*keys)
45
+ permitted_keys.merge keys
46
+ self
47
+ end
48
+
49
+ # Checks that only the the previously mentioned keys exist
50
+ # In Rails, `permit' will do this as well, but having this as a separate
51
+ # option allows for specifying permit not at the beginning of the chain
52
+ def only
53
+ unless (extra_keys = hash_keys - permitted_keys).empty?
54
+ error "extra #{terms} #{extra_keys.to_a}" + postscript
55
+ end
56
+ self
57
+ end
58
+
59
+ # Similar to Rails' `permit' method.
60
+ def permit_only(*keys)
61
+ permit(*keys).only
62
+ end
63
+
64
+ def int(*keys)
65
+ permitted_keys.merge keys
66
+ keys.each do |key|
67
+ next if hash[key].is_a? Integer
68
+ error "#{term} #{key.inspect} must be integer but was #{hash[key].inspect}" + postscript
69
+ end
70
+ self
71
+ end
72
+
73
+ def int_or_nil(*keys)
74
+ permitted_keys.merge keys
75
+ keys.each do |key|
76
+ next if hash[key].nil? || hash[key].is_a?(Integer)
77
+ error "#{term} #{key.inspect} must be integer but was #{hash[key].inspect}" + postscript
78
+ end
79
+ self
80
+ end
81
+
82
+ def not_nil(*keys)
83
+ permitted_keys.merge keys
84
+ keys.each do |key|
85
+ next unless hash[key].nil?
86
+ error "#{term} #{key.inspect} is nil" + postscript
87
+ end
88
+ self
89
+ end
90
+
91
+ private
92
+
93
+ def hash
94
+ @string_keys ? @hash : @hash.symbolize_keys
95
+ end
96
+
97
+ def hash_keys
98
+ (@string_keys ? @hash.keys : @hash.keys.map(&:to_sym)).to_set
99
+ end
100
+
101
+ attr_reader :permitted_keys, :term
102
+
103
+ def error(message)
104
+ raise @error_class, message
105
+ end
106
+
107
+ def postscript
108
+ "\n\tin #{hash.inspect}"
109
+ end
110
+
111
+ def terms
112
+ @terms ||= term.pluralize
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,3 @@
1
+ module HashControl
2
+ VERSION = "0.1.2"
3
+ end
@@ -0,0 +1,3 @@
1
+ require_relative 'hash_control/model'
2
+ require_relative 'hash_control/validator'
3
+ require_relative 'hash_control/version'
@@ -0,0 +1,96 @@
1
+ require 'hash_control/model'
2
+
3
+ describe HashControl::Model do
4
+ before :all do
5
+ class Comment
6
+ include ::HashControl::Model
7
+ require_key :author, :body, :date
8
+ permit_key :image
9
+ end
10
+
11
+ class Something
12
+ include ::HashControl::Model
13
+ require_key :id
14
+ permit_all_keys
15
+ end
16
+ end
17
+
18
+ describe "making an instance should error if it" do
19
+ it "does not permit all keys and has extra params" do
20
+ expect {
21
+ Comment.new(body: "this ain't gonna fly", author: 'me', date: Time.now, extra: 'hullo')
22
+ }.to raise_error(ArgumentError)
23
+ # ArgumentError: extra params [:extra]
24
+ # in {:body=>"this ain't gonna fly", :author=>"me", :date=>2014-01-01 00:00:00 -0000, :extra=>"hullo"}
25
+ end
26
+
27
+ it "is missing required params" do
28
+ expect {
29
+ Something.new(body: "this won't either")
30
+ }.to raise_error(ArgumentError)
31
+ # ArgumentError: required params [:id] missing
32
+ # in {:body=>"this won't either"}
33
+ end
34
+ end
35
+
36
+ describe "a valid model" do
37
+ describe "that permits only certain keys" do
38
+ before :all do
39
+ @comment = Comment.new(author: 'me', body: 'interesting stuff', date: Time.now)
40
+ end
41
+
42
+ describe "can use methods to access" do
43
+ it "explicitly required and permitted keys" do
44
+ expect(@comment.author).to eq('me')
45
+ expect(@comment.body).to eq('interesting stuff')
46
+ expect(@comment.date.class).to eq(Time)
47
+ expect(@comment.image).to eq(nil)
48
+ end
49
+ it "not other keys" do
50
+ expect{@comment.nonexistent}.to raise_error(NoMethodError)
51
+ end
52
+ end
53
+
54
+ describe "can use [] (using both string and symbol) to access" do
55
+ it "all keys regardless of whether they exist" do
56
+ expect(@comment['author']).to eq('me')
57
+ expect(@comment[:author]).to eq('me')
58
+ expect(@comment['image']).to eq(nil)
59
+ expect(@comment[:image]).to eq(nil)
60
+ expect(@comment['nonexistent']).to eq(nil)
61
+ expect(@comment[:nonexistent]).to eq(nil)
62
+ end
63
+ end
64
+
65
+ end
66
+
67
+ describe "that permits all keys" do
68
+ before :all do
69
+ @something = Something.new(id: 1, body: "heh")
70
+ end
71
+
72
+ describe "can use methods to access" do
73
+ it "explicitly required and permitted keys" do
74
+ expect(@something.id).to eq(1)
75
+ end
76
+ it "not implicitly-permitted keys" do
77
+ expect{@something.body}.to raise_error(NoMethodError)
78
+ expect{@something.nonexistent}.to raise_error(NoMethodError)
79
+ end
80
+ it "not other keys" do
81
+ expect{@something.body}.to raise_error(NoMethodError)
82
+ expect{@something.nonexistent}.to raise_error(NoMethodError)
83
+ end
84
+ end
85
+
86
+ describe "can use [] (using both string and symbol) to access" do
87
+ it "all keys regardless of whether they exist" do
88
+ expect(@something['body']).to eq('heh')
89
+ expect(@something[:body]).to eq('heh')
90
+ expect(@something['nonexistent']).to eq(nil)
91
+ expect(@something[:nonexistent]).to eq(nil)
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,83 @@
1
+ require 'hash_control/validator'
2
+
3
+ describe HashControl::Validator do
4
+ def validate(hash)
5
+ ::HashControl::Validator.new(hash)
6
+ end
7
+
8
+ before :all do
9
+ @empty = {
10
+ }
11
+ @get_request = {
12
+ get: '/api/article/23/comment/34/show'
13
+ }
14
+ @post_request = {
15
+ post: '/api/article/23/comment/34/update',
16
+ body: {
17
+ date: Time.new,
18
+ body: 'hullo',
19
+ meta: 'fsdkfsifhdsfsdhkj'
20
+ }
21
+ }
22
+ @not_allowed_but_currently_ok = {
23
+ get: 'something',
24
+ post: 'something_else'
25
+ }
26
+ end
27
+
28
+ describe "when used as its own class," do
29
+ # `require` ensures certain keys are present
30
+ it "require should work properly" do
31
+ expect{ validate(@post_request).require(:post) }.not_to raise_error
32
+ expect{ validate(@post_request).require(:body) }.not_to raise_error
33
+ expect{ validate(@post_request).require(:post, :body) }.not_to raise_error
34
+ expect{ validate(@post_request).require(:nonexistent) }.to raise_error(ArgumentError)
35
+ end
36
+
37
+ # `require_n_of` ensures at least n of certain keys are present
38
+ it "require_n_of should work properly" do
39
+ expect{ validate(@post_request).require_n_of(2, :post, :body) }.not_to raise_error
40
+ end
41
+
42
+ # `permit` marks keys as allowed but doesn't do any verification
43
+ it "permit should work properly" do
44
+ expect{ validate(@post_request).permit(:body) }.not_to raise_error
45
+ expect{ validate(@post_request).permit(:nonexistent) }.not_to raise_error
46
+ end
47
+
48
+ # `only` ensures no other keys are present
49
+ it "only should work properly" do
50
+ expect{ validate(@post_request).only }.to raise_error(ArgumentError)
51
+ expect{ validate(@post_request).require(:post).only }.to raise_error(ArgumentError)
52
+ expect{ validate(@post_request).require(:post).permit(:body).only }.not_to raise_error
53
+ end
54
+ end
55
+
56
+ describe "when subclassed," do
57
+ before :all do
58
+ class CustomValidator < ::HashControl::Validator
59
+ def validate_request
60
+ require_one_of(:get, :post)
61
+ end
62
+
63
+ def validate_get_request
64
+ validate_request.only
65
+ end
66
+
67
+ def validate_post_request
68
+ validate_request.permit(:body).only
69
+ end
70
+ end
71
+ end
72
+
73
+ it "should work properly for allowed hashes" do
74
+ expect{ CustomValidator.new(@get_request).validate_get_request }.not_to raise_error
75
+ expect{ CustomValidator.new(@post_request).validate_post_request }.not_to raise_error
76
+ expect{ CustomValidator.new(@not_allowed_but_currently_ok).validate_post_request }.not_to raise_error
77
+ end
78
+
79
+ it "should work properly for invalid hashes" do
80
+ expect{ CustomValidator.new(@empty).validate_get_request }.to raise_error(ArgumentError)
81
+ end
82
+ end
83
+ end
metadata ADDED
@@ -0,0 +1,72 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: hash_control
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.2
5
+ platform: ruby
6
+ authors:
7
+ - Sean Zhu
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-08-22 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: '4.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '4.0'
27
+ description: |2
28
+ Provides some conveniences for validating and manipulating hash-like data.
29
+ email: interestinglythere@gmail.com
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - ".gitignore"
35
+ - ".rspec"
36
+ - ".travis.yml"
37
+ - Gemfile
38
+ - LICENSE
39
+ - Makefile
40
+ - README.md
41
+ - hash_control.gemspec
42
+ - lib/hash_control.rb
43
+ - lib/hash_control/model.rb
44
+ - lib/hash_control/validator.rb
45
+ - lib/hash_control/version.rb
46
+ - spec/lib/hash_control/model_spec.rb
47
+ - spec/lib/hash_control/validator_spec.rb
48
+ homepage: https://github.com/szhu/hashcontrol
49
+ licenses:
50
+ - MIT
51
+ metadata: {}
52
+ post_install_message:
53
+ rdoc_options: []
54
+ require_paths:
55
+ - lib
56
+ required_ruby_version: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ required_rubygems_version: !ruby/object:Gem::Requirement
62
+ requirements:
63
+ - - ">="
64
+ - !ruby/object:Gem::Version
65
+ version: '0'
66
+ requirements: []
67
+ rubyforge_project:
68
+ rubygems_version: 2.2.2
69
+ signing_key:
70
+ specification_version: 4
71
+ summary: Conveniently validating and manipulating hash-like data.
72
+ test_files: []