simple_params 0.0.1pre2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ NzljZWZmNDczMjI4ZDk5ZDg2NmYxNzRlNzQ5ZjhjNDU4YjJhMmVjNw==
5
+ data.tar.gz: !binary |-
6
+ ZWQ0MGE5MjE4NmQ4MDUxYzMxNmUwMjM4ZWU4NDk0YWQyYTJkNWZmNw==
7
+ SHA512:
8
+ metadata.gz: !binary |-
9
+ ODJkOTE3YzRjNTUzOGQ3ZGE0MDFlYzg5Mzc3YzNjMmI4NDJlZTA4OGZlYzkw
10
+ MDkzZDlmMmNkZjY1YmZlNjc0OGJhZTk2MGJiNTEwMjc0NDcyZWFmNzJiMmZi
11
+ MjVlNzFjZTU0NWY0NGY5ODg5M2RmYmQ0MTQ0ZjNhMTgzMTQ3ZjU=
12
+ data.tar.gz: !binary |-
13
+ NDhlYzBmNDhlOGYzYjIyNzY2NDQ2ZmViNmFmOTA2YzQ1NmViNGJlOTEwOTg1
14
+ OTc4ODg2NDlmYzRkZGRlY2M4NTdjYTBhZjE0NWMzMWRmNTgwZWRmNjI5ZGY5
15
+ ZjFkY2I1MzliODY4Y2Q2N2I5ODVmYWM5ZDBmNDc4MzRiOWY2OTQ=
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format progress
data/.ruby-gemset ADDED
@@ -0,0 +1 @@
1
+ simple_params_gem
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ ruby-1.9.3-p545
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,60 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ simple_params (0.0.1pre)
5
+ activemodel (>= 3.0, < 5.0)
6
+ virtus (>= 1.0.0)
7
+
8
+ GEM
9
+ remote: https://rubygems.org/
10
+ specs:
11
+ activemodel (4.2.0)
12
+ activesupport (= 4.2.0)
13
+ builder (~> 3.1)
14
+ activesupport (4.2.0)
15
+ i18n (~> 0.7)
16
+ json (~> 1.7, >= 1.7.7)
17
+ minitest (~> 5.1)
18
+ thread_safe (~> 0.3, >= 0.3.4)
19
+ tzinfo (~> 1.1)
20
+ axiom-types (0.1.1)
21
+ descendants_tracker (~> 0.0.4)
22
+ ice_nine (~> 0.11.0)
23
+ thread_safe (~> 0.3, >= 0.3.1)
24
+ builder (3.2.2)
25
+ coercible (1.0.0)
26
+ descendants_tracker (~> 0.0.1)
27
+ descendants_tracker (0.0.4)
28
+ thread_safe (~> 0.3, >= 0.3.1)
29
+ diff-lcs (1.2.5)
30
+ equalizer (0.0.9)
31
+ i18n (0.7.0)
32
+ ice_nine (0.11.1)
33
+ json (1.8.2)
34
+ minitest (5.5.1)
35
+ rake (10.4.2)
36
+ rspec (2.99.0)
37
+ rspec-core (~> 2.99.0)
38
+ rspec-expectations (~> 2.99.0)
39
+ rspec-mocks (~> 2.99.0)
40
+ rspec-core (2.99.2)
41
+ rspec-expectations (2.99.2)
42
+ diff-lcs (>= 1.1.3, < 2.0)
43
+ rspec-mocks (2.99.3)
44
+ thread_safe (0.3.4)
45
+ tzinfo (1.2.2)
46
+ thread_safe (~> 0.1)
47
+ virtus (1.0.4)
48
+ axiom-types (~> 0.1)
49
+ coercible (~> 1.0)
50
+ descendants_tracker (~> 0.0, >= 0.0.3)
51
+ equalizer (~> 0.0, >= 0.0.9)
52
+
53
+ PLATFORMS
54
+ ruby
55
+
56
+ DEPENDENCIES
57
+ bundler (~> 1.5)
58
+ rake (~> 10.1)
59
+ rspec (~> 2.6)
60
+ simple_params!
data/README.md ADDED
@@ -0,0 +1,161 @@
1
+ # Simple Params
2
+
3
+ A simple class to handle parameter validation, for use in APIs or Service Objects. Simply pass in your JSON hash, and get a simple, validateable, accessible ActiveModel-like object.
4
+
5
+ This class provides the following benefits for handling params:
6
+ * Access via array-like (params[:person][:name]), or struct-like (params.person.name) syntax
7
+ * Ability to validate with any ActiveModel validation
8
+ * ActiveModel-like errors, including nested error objects for nested params
9
+ * Parameter type-coercion (e.g. transform "1" into the Integer 1)
10
+
11
+ ## Installation
12
+
13
+ Add this line to your application's Gemfile:
14
+
15
+ gem 'simple_params', '~> 0.0.1pre'
16
+
17
+ And then execute:
18
+
19
+ $ bundle
20
+
21
+ Or install it yourself as:
22
+
23
+ $ gem install simple_params
24
+
25
+ ## Defining your Params class
26
+
27
+ All you need to do is create a class to specify accepted parameters and validations
28
+
29
+ ```ruby
30
+ class MyParams < SimpleParams::Params
31
+ param :name
32
+ param :age, type: Integer
33
+ param :date_of_birth, type: Date, optional: true
34
+ param :hair_color, default: "brown", validations: { inclusion: { in: ["brown", "red", "blonde", "white"] }}
35
+
36
+ nested_hash :address do
37
+ param :street
38
+ param :city
39
+ param :zip_code, validations: { length: { in: 5..9 } }
40
+ param :state, optional: true
41
+ param :country, default: "USA"
42
+ end
43
+
44
+ # You can include whatever custom methods you need as well
45
+ def full_address
46
+ [address.street, address.city, address.state, address.zip_code].join(" ")
47
+ end
48
+ end
49
+ ```
50
+ We can now treat these params in very ActiveModel-like ways. For example:
51
+
52
+ ```ruby
53
+ params = MyParams.new(
54
+ {
55
+ name: "Bob Barker",
56
+ age: "91",
57
+ date_of_birth: "December 12th, 1923",
58
+ hair_color: "white",
59
+ address: {
60
+ street: "7800 Beverly Blvd",
61
+ city: "Los Angeles",
62
+ state: "California",
63
+ zip_code: "90036"
64
+ }
65
+ }
66
+ )
67
+
68
+ params.valid? #=> true
69
+ params.name #=> "Bob Barker"
70
+ params.full_address #=> "7800 Beverly Blvd Los Angeles California 90036"
71
+ ```
72
+
73
+ ## Validation & Errors
74
+
75
+ Errors are also treated in a very ActiveModel-like way, making it simple to validate even complexly nested inputs.
76
+
77
+ ```ruby
78
+ params = MyParams.new(
79
+ {
80
+ name: "",
81
+ age: "91",
82
+ address: {
83
+ city: "Los Angeles",
84
+ state: "California",
85
+ zip_code: "90036"
86
+ }
87
+ }
88
+ )
89
+
90
+ params.valid? #=> false
91
+ params.errors[:name] #=> ["can't be blank"]
92
+ params.errors[:address][:street] #=> ["can't be blank"]
93
+ params.address.errors[:street] #=> ["can't be blank"]
94
+
95
+ params.errors.as_json #=> {:name=>["can't be blank"], :address=>{:street=>["can't be blank"]}}
96
+ params.address.errors.as_json #=> {:street=>["can't be blank"]}
97
+ ```
98
+
99
+ ## Defaults
100
+
101
+ It is easy to set simple or complex defaults, with either a static value or a Proc
102
+
103
+ ```ruby
104
+ class DefaultParams < SimpleParams::Params
105
+ param :name, default: "Doc Brown"
106
+ param :first_initial, default: lambda { |params, attribute| params.name[0] }
107
+
108
+ nested_hash :car do
109
+ param :make, default: "DeLorean"
110
+ param :license_plate, default: lambda { |params, attribute| params.make[0..2].upcase + "-1234" }
111
+ end
112
+ end
113
+
114
+ params = DefaultParams.new
115
+
116
+ params.name #=> "Doc Brown"
117
+ params.first_initial #=> "D"
118
+ params.car.make #=> "DeLorean"
119
+ params.car.license_plate #=> "DEL-1234"
120
+ ```
121
+
122
+ # Coercion
123
+
124
+ SimpleParams provides support for converting incoming params from one type to another. This is extremely helpful for integers, dates, floats, and booleans, which will often come in as strings but should not be treated as such.
125
+
126
+ By default, params are assumed to be strings, so there is no need to specify String as a type.
127
+
128
+ ```ruby
129
+ class CoercionParams < SimpleParams::Params
130
+ param :name
131
+ param :age, type: Integer
132
+ param :date_of_birth, type: Date
133
+ param :pocket_change, type: BigDecimal
134
+ end
135
+
136
+ params = CoercionParams.new(name: "Bob", age: "21", date_of_birth: "June 1st, 1980", pocket_change: "2.35")
137
+
138
+ params.name #=> "Bob"
139
+ params.age #=> 21
140
+ params.date_of_birth #=> #<Date: 1980-06-01>
141
+ params.pocket_change #=> #<BigDecimal:89ed240,'0.235E1',18(18)>
142
+ ```
143
+
144
+ SimpleParams also provide helper methods for implicitly specifying the type, if you prefer that syntax. Here is the same class as above, but redefined with these helper methods.
145
+
146
+ ```ruby
147
+ class CoercionParams < SimpleParams::Params
148
+ param :name
149
+ integer_param :age
150
+ date_param :date_of_birth
151
+ decimal_param :pocket_change
152
+ end
153
+ ```
154
+
155
+ ## Contributing
156
+
157
+ 1. Fork it
158
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
159
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
160
+ 4. Push to the branch (`git push origin my-new-feature`)
161
+ 5. Create new Pull Request
@@ -0,0 +1,133 @@
1
+ require "active_model"
2
+
3
+ module SimpleParams
4
+ class Errors < ActiveModel::Errors
5
+
6
+ def initialize(base, nested_attributes = [])
7
+ super(base)
8
+ @nested_attributes = symbolize_nested(nested_attributes)
9
+ end
10
+
11
+ def [](attribute)
12
+ get(attribute.to_sym) || reset_attribute(attribute.to_sym)
13
+ end
14
+
15
+ def []=(attribute, error)
16
+ add_error_to_attribute(attribute, error)
17
+ end
18
+
19
+ def add(attribute, message = :invalid, options = {})
20
+ message = normalize_message(attribute, message, options)
21
+ if exception = options[:strict]
22
+ exception = ActiveModel::StrictValidationFailed if exception == true
23
+ raise exception, full_message(attribute, message)
24
+ end
25
+
26
+ add_error_to_attribute(attribute, message)
27
+ end
28
+
29
+ def clear
30
+ super
31
+ @nested_attributes.each do |attribute|
32
+ if fetch_nested_attribute(attribute).present?
33
+ fetch_nested_attribute(attribute).errors.clear
34
+ end
35
+ end
36
+ end
37
+
38
+ def empty?
39
+ super &&
40
+ @nested_attributes.all? do |attribute|
41
+ if fetch_nested_attribute(attribute).present?
42
+ fetch_nested_attribute(attribute).errors.empty?
43
+ end
44
+ end
45
+ end
46
+ alias_method :blank?, :empty?
47
+
48
+ def include?(attribute)
49
+ if fetch_nested_attribute(attribute)
50
+ !fetch_nested_attribute(attribute).errors.empty?
51
+ else
52
+ messages[attribute].present?
53
+ end
54
+ end
55
+ alias_method :has_key?, :include?
56
+ alias_method :key?, :include?
57
+
58
+ def values
59
+ messages.values +
60
+ @nested_attributes.map do |attribute|
61
+ if fetch_nested_attribute(attribute).present?
62
+ fetch_nested_attribute(attribute).errors.values
63
+ end
64
+ end
65
+ end
66
+
67
+ def full_messages
68
+ parent_messages = map { |attribute, message| full_message(attribute, message) }
69
+ nested_messages = @nested_attributes.map do |attribute|
70
+ if fetch_nested_attribute(attribute)
71
+ messages = fetch_nested_attribute(attribute).errors.full_messages
72
+ messages.map do |message|
73
+ "#{attribute} " + message
74
+ end
75
+ end
76
+ end
77
+ (parent_messages + nested_messages).flatten
78
+ end
79
+
80
+ def to_hash(full_messages = false)
81
+ messages = super(full_messages)
82
+ nested_messages = @nested_attributes.map do |attribute|
83
+ errors = nested_error_messages(attribute, full_messages)
84
+ unless errors.empty?
85
+ messages.merge!(attribute.to_sym => errors)
86
+ end
87
+ end
88
+ messages
89
+ end
90
+
91
+ private
92
+ def nested_error_messages(attribute, full_messages = false)
93
+ if fetch_nested_attribute(attribute)
94
+ if full_messages
95
+ errors = fetch_nested_attribute(attribute).errors
96
+ errors.messages.each_with_object({}) do |(attribute, array), messages|
97
+ messages[attribute] = array.map { |message| errors.full_message(attribute, message) }
98
+ end
99
+ else
100
+ fetch_nested_attribute(attribute).errors.messages.dup
101
+ end
102
+ else
103
+ {}
104
+ end
105
+ end
106
+
107
+ def add_error_to_attribute(attribute, error)
108
+ if fetch_nested_attribute(attribute)
109
+ fetch_nested_attribute(attribute).errors[:base] = error
110
+ else
111
+ self[attribute] << error
112
+ end
113
+ end
114
+
115
+ def reset_attribute(attribute)
116
+ if fetch_nested_attribute(attribute)
117
+ set(attribute.to_sym, fetch_nested_attribute(attribute).errors)
118
+ else
119
+ set(attribute.to_sym, [])
120
+ end
121
+ end
122
+
123
+ def fetch_nested_attribute(attribute)
124
+ if @nested_attributes.include?(attribute)
125
+ @base.send(attribute)
126
+ end
127
+ end
128
+
129
+ def symbolize_nested(nested)
130
+ nested.map { |x| x.to_sym }
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,129 @@
1
+ require "active_model"
2
+ require "virtus"
3
+
4
+ module SimpleParams
5
+ class Params
6
+ include Virtus.model
7
+ include ActiveModel::Validations
8
+ include SimpleParams::Validations
9
+
10
+ class << self
11
+ TYPE_MAPPINGS = {
12
+ integer: Integer,
13
+ string: String,
14
+ decimal: BigDecimal,
15
+ datetime: DateTime,
16
+ date: Time,
17
+ time: DateTime,
18
+ float: Float,
19
+ # See note on Virtus
20
+ boolean: Axiom::Types::Boolean,
21
+ array: Array,
22
+ hash: Hash
23
+ }
24
+
25
+ TYPE_MAPPINGS.each_pair do |sym, klass|
26
+ define_method("#{sym}_param") do |name, opts={}|
27
+ param(name, opts.merge(type: klass))
28
+ end
29
+ end
30
+
31
+ def param(name, opts={})
32
+ define_attribute(name, opts)
33
+ add_validations(name, opts)
34
+ end
35
+
36
+ def nested_hash(name, opts={}, &block)
37
+ attr_accessor name
38
+ nested_class = define_nested_class(&block)
39
+ @nested_hashes ||= {}
40
+ @nested_hashes[name.to_sym] = nested_class
41
+ end
42
+ alias_method :nested_param, :nested_hash
43
+ alias_method :nested, :nested_hash
44
+
45
+ def nested_hashes
46
+ @nested_hashes || {}
47
+ end
48
+
49
+ private
50
+ def define_attribute(name, opts = {})
51
+ type = opts[:type] || String
52
+ default = opts[:default]
53
+ if default.present?
54
+ attribute name, type, default: default
55
+ else
56
+ attribute name, type
57
+ end
58
+ end
59
+
60
+ def add_validations(name, opts = {})
61
+ validations = opts[:validations] || {}
62
+ validations.merge!(presence: true) unless opts[:optional]
63
+ validates name, validations unless validations.empty?
64
+ end
65
+
66
+ def define_nested_class(&block)
67
+ Class.new(Params).tap do |klass|
68
+ name_function = Proc.new {
69
+ def self.model_name
70
+ ActiveModel::Name.new(self, nil, "temp")
71
+ end
72
+ }
73
+ klass.class_eval(&name_function)
74
+ klass.class_eval(&block)
75
+ end
76
+ end
77
+ end
78
+
79
+ def initialize(params={}, parent = nil)
80
+ @parent = parent
81
+ @original_params = hash_to_symbolized_hash(params)
82
+ @nested_params = nested_hashes.keys
83
+ @errors = SimpleParams::Errors.new(self, @nested_params)
84
+ initialize_nested_classes
85
+ set_accessors(params)
86
+ # This method comes from Virtus
87
+ # virtus/lib/virtus/instance_methods.rb
88
+ set_default_attributes
89
+ end
90
+
91
+ protected
92
+ def set_accessors(params={})
93
+ params.each do |attribute_name, value|
94
+ # Don't set accessors for nested classes
95
+ unless value.is_a?(Hash)
96
+ send("#{attribute_name}=", value)
97
+ reset_blank_attributes
98
+ end
99
+ end
100
+ end
101
+
102
+ def reset_blank_attributes
103
+ # Reset to the default value for blank attributes
104
+ attributes.each do |attribute_name, value|
105
+ if send(attribute_name).blank?
106
+ # This method comes from Virtus
107
+ # virtus/lib/virtus/instance_methods.rb
108
+ reset_attribute(attribute_name)
109
+ end
110
+ end
111
+ end
112
+
113
+ private
114
+ def hash_to_symbolized_hash(hash)
115
+ hash.inject({}){|memo,(k,v)| memo[k.to_sym] = v; memo}
116
+ end
117
+
118
+ def nested_hashes
119
+ self.class.nested_hashes
120
+ end
121
+
122
+ def initialize_nested_classes
123
+ nested_hashes.each do |key, klass|
124
+ initialization_params = @original_params[key.to_sym] || {}
125
+ send("#{key}=", klass.new(initialization_params, self))
126
+ end
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,28 @@
1
+ require "active_model"
2
+
3
+ module SimpleParams
4
+ module Validations
5
+ extend ActiveModel::Validations
6
+
7
+ # Overriding #valid? to provide recursive validating of
8
+ # nested params
9
+ def valid?(context = nil)
10
+ current_context, self.validation_context = validation_context, context
11
+ errors.clear
12
+ run_validations!
13
+ nested_hashes.each do |key, value|
14
+ nested_class = send("#{key}")
15
+ nested_class.valid?
16
+ end
17
+ errors.empty?
18
+ ensure
19
+ self.validation_context = current_context
20
+ end
21
+
22
+ def validate!
23
+ unless valid?
24
+ raise StandardError, errors.to_s
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,3 @@
1
+ module SimpleParams
2
+ VERSION = "0.0.1pre2"
3
+ end
@@ -0,0 +1,4 @@
1
+ require 'simple_params/version'
2
+ require 'simple_params/errors'
3
+ require 'simple_params/validations'
4
+ require 'simple_params/params'
@@ -0,0 +1,26 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'simple_params/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "simple_params"
8
+ spec.version = SimpleParams::VERSION
9
+ spec.authors = ["brycesenz"]
10
+ spec.email = ["bryce.senz@gmail.com"]
11
+ spec.description = %q{Simple way to specify API params}
12
+ spec.summary = %q{A DSL for specifying params, including type coercion and validation}
13
+ spec.homepage = "https://github.com/brycesenz/simple_params"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
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_dependency "activemodel", ">= 3.0", "< 5.0"
22
+ spec.add_dependency "virtus", ">= 1.0.0"
23
+ spec.add_development_dependency "bundler", "~> 1.5"
24
+ spec.add_development_dependency "rake", "~> 10.1"
25
+ spec.add_development_dependency "rspec", "~> 2.6"
26
+ end