attributes_dsl 0.0.2 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: a0a8b2957544474ff4116fd2b6d1703de977ba74
4
- data.tar.gz: b61d244f9ea674fdff84404e269dfa083fca6f64
3
+ metadata.gz: 7c399823c440258ebcd951ce049e16877934b630
4
+ data.tar.gz: 345a83666686089656e22eb5a38f88997baccd41
5
5
  SHA512:
6
- metadata.gz: de35390efe7b4feb77a049d040884615e11c5de61e9d8cb3bb595fbf2b71a0b0e3d53b5dd78425c6e775774366bb39cd538b5b6fa3e1af6c67e61ec03863669d
7
- data.tar.gz: 213cac8c92f35d396ce1272b53baace3a03ee635521e573cf039c85fd72bf4f43f384f46ae7d958188f9ca1d6c3424f82bc992e82c94b7fecd5221c6f10fbff4
6
+ metadata.gz: a25c08685b444464ad501a9a0a5a9d945ea854bef5959241b53918470a7dbda28d0c173aec1c6a57709ad637c490ec8864bafad9f2c1cc5a6d622d56450a667f
7
+ data.tar.gz: 36a64b86b318bbad9cfbceddefdf9fb49616f2443f259a2024642461fdb03d6eb143216913e24cfd402938c37ef38c6ea8cce2da8b1f42eec67022931cb40aac
data/.travis.yml CHANGED
@@ -4,14 +4,10 @@ bundler_args: --without metrics --without benchmarks
4
4
  cache: bundler
5
5
  script: bundle exec rake test:coverage:run
6
6
  rvm:
7
- - '1.9.3'
8
- - '2.0'
9
7
  - '2.1'
10
8
  - '2.2'
11
9
  - ruby-head
12
- - rbx-2 --1.9
13
10
  - rbx-2 --2.1
14
- - jruby-1.7-19mode
15
11
  - jruby-9.0.0.0
16
12
  - jruby-head
17
13
  matrix:
data/CHANGELOG.md CHANGED
@@ -1,3 +1,26 @@
1
+ ## version 0.1.0 2015-12-08
2
+
3
+ Stabilized DSL
4
+
5
+ ### Added
6
+
7
+ * Option `:reader` that allows skipping attribute reader (nepalez)
8
+ * Option `:only` whitelists allowed values (nepalez)
9
+ * Option `:except` blacklists allowed values (nepalez)
10
+ * Option `:coercer` is an alternative to block syntax (nepalez)
11
+ * An instance can be initialized by hash with both symbolic and string keys (nepalez)
12
+
13
+ ### Deleted
14
+
15
+ * Support for rubies < 2.1
16
+
17
+ ### Internal
18
+
19
+ * Removed freezing of values (nepalez)
20
+ * Slightly (20-25%) improved efficiency due to usage of transpfoc (nepalez)
21
+
22
+ [Compare v0.0.2...v0.1.0](https://github.com/nepalez/attributes_dsl/compare/v0.0.2...v0.1.0)
23
+
1
24
  ## version 0.0.2 2015-09-11
2
25
 
3
26
  This version is a result of applying v0.0.1 to the existing gems:
data/README.md CHANGED
@@ -21,40 +21,21 @@ require "attributes_dsl"
21
21
  class User
22
22
  extend AttributesDSL
23
23
 
24
- # `name` is required should be symbolized
25
- attribute :name, required: true do |value|
26
- value.to_s.to_sym
27
- end
28
-
29
- # `sex` is optional and set to `:male` by default.
30
- # It can be set to either :male or :female
31
- attribute :sex, default: :male do |value|
32
- (value == :male )? :male : :female
33
- end
34
-
35
- # `age` is optional and set to `nil` by default.
36
- # Then it is converted to integer
37
- attribute :age, &:to_i
38
-
39
- # `position` is optional and set to `nil` by default
40
- attribute :position
41
-
42
- # All other attributes are ignored
24
+ attribute :name, required: true, coerce: -> v { v.to_s }
25
+ attribute :sex, default: :male, only: /male|female/
26
+ attribute :age, only: 18..25
27
+ attribute :city, reader: false, except: %w(Moscow)
43
28
  end
44
29
 
45
- user = User.new(name: "Jane", sex: :women, age: "26", place: "Moscow")
30
+ user = User.new(name: :Jane, sex: :female, age: 24, city: "Kiev")
46
31
  user.attributes
47
- # => { name: :Jane, sex: :female, age: 26, position: nil }
32
+ # => { name: :Jane, sex: :female, age: 26, city: "Kiev" }
48
33
 
49
34
  # Aliases for attributes[:some_attribute]
50
- user.name # => :Jane
51
- user.sex # => :female
52
- user.age # => 26
53
- user.position # => nil
54
-
55
- # Required attributes should be assigned:
56
- user = User.new(sex: :women, age: "26", place: "Moscow")
57
- # => #<ArgumentError "Undefined attributes: name">
35
+ user.name # => "Jane"
36
+ user.sex # => :female
37
+ user.age # => 26
38
+ user.city # => #<NoMethodError ...>
58
39
  ```
59
40
 
60
41
  Additional Details
@@ -62,10 +43,14 @@ Additional Details
62
43
 
63
44
  ### Attribute declaration
64
45
 
65
- The `attribute` class method takes the `name` and 2 options:
46
+ The `attribute` class method takes the `name` and 3 options:
66
47
 
67
- - `:default` for the default value (otherwise `nil`);
48
+ - `:default` for the default value (otherwise `nil`).
68
49
  - `:required` to declare the attribute as required. It will be ignored if a default value is provided!
50
+ - `:reader` defines whether the attribute reader should be defined (`true` by default).
51
+ - `:only` defines allowed values. You can use procs (`-> v { v.to_s }`), ranges (`18..25`), regexps (`/male|female/`), constants (`String`), or arrays (`[:male, :female]`) to define a restriction.
52
+ - `:except` defines forbidden values.
53
+ - `:coercer` defines a procedure to convert assigned value.
69
54
 
70
55
  It is also takes the block, used to coerce a value. The coercer is applied to the default value too.
71
56
 
@@ -75,10 +60,9 @@ Instance methods (like `#name`) are just aliases for the corresponding value of
75
60
 
76
61
  ```ruby
77
62
  user = User.new(name: "John")
78
- user.attributes # => { name: :John, sex: :male, age: 0, position: nil }
79
- user.name # => :John
63
+ user.attributes # => { name: :John, sex: :male, age: nil, city: nil }
80
64
 
81
- # but
65
+ user.name # => :John
82
66
  user.instance_variable_get :@name # => nil
83
67
  ```
84
68
 
@@ -93,7 +77,8 @@ end
93
77
 
94
78
  user = UserWithRole.new(name: "Sam")
95
79
  user.attributes
96
- # => { name: :Sam, sex: :male, age: 0, position: nil, role: :user }
80
+ user.attributes
81
+ # => { name: :John, sex: :male, age: nil, city: nil, role: :user }
97
82
  ```
98
83
 
99
84
  ### Undefining Attributes
@@ -160,29 +145,21 @@ The results are following:
160
145
 
161
146
  ```
162
147
  -------------------------------------------------
163
- anima 211.638k (± 3.7%) i/s - 1.071M
164
- kwattr 187.276k (± 3.6%) i/s - 947.484k
165
- fast_attributes 160.916k (± 2.4%) i/s - 816.726k
166
- attributes_dsl 71.850k (± 3.0%) i/s - 365.365k
167
- active_attr 71.489k (± 3.6%) i/s - 357.995k
168
- virtus 45.554k (± 7.1%) i/s - 229.338k
169
-
170
- Comparison:
171
- anima: 211637.9 i/s
172
- kwattr: 187276.2 i/s - 1.13x slower
173
- fast_attributes: 160916.1 i/s - 1.32x slower
174
- attributes_dsl: 71850.0 i/s - 2.95x slower
175
- active_attr: 71489.1 i/s - 2.96x slower
176
- virtus: 45553.8 i/s - 4.65x slower
148
+ kwattr: 183416.9 i/s
149
+ anima: 169647.3 i/s - 1.08x slower
150
+ fast_attributes: 156036.2 i/s - 1.18x slower
151
+ attributes_dsl: 74495.9 i/s - 2.46x slower
152
+ active_attr: 74469.4 i/s - 2.46x slower
153
+ virtus: 46587.0 i/s - 3.94x slower
177
154
  ```
178
155
 
179
156
  Results above are pretty reasonable.
180
157
 
181
158
  The gem is faster than `virtus` that has many additional features.
182
159
 
183
- It is as fast as `active_attrs` (but has more customizable coercers).
160
+ It is as fast as `active_attrs` (but has more options).
184
161
 
185
- It is 2 times slower than `fast_attributes` that has no coercer and default values. And it is 3 times slower than `anima` and `kwattr` that provides only the base settings.
162
+ It is 2 times slower than `fast_attributes` that has no coercer and default values. And it is 2.5 times slower than `anima` and `kwattr` that provide only simple attribute's declaration.
186
163
 
187
164
  Installation
188
165
  ------------
@@ -209,12 +186,10 @@ gem install attributes_dsl
209
186
  Compatibility
210
187
  -------------
211
188
 
212
- Tested under rubies [compatible to MRI 1.9+][versions].
189
+ Tested under rubies [compatible to MRI 2.1+][versions].
213
190
 
214
191
  Uses [RSpec][rspec] 3.0+ for testing and [hexx-suit][hexx-suit] for dev/test tools collection.
215
192
 
216
- 100% [mutant]-proof covered.
217
-
218
193
  Contributing
219
194
  ------------
220
195
 
@@ -222,7 +197,6 @@ Contributing
222
197
  * [Fork the project](https://github.com/nepalez/attributes_dsl)
223
198
  * Create your feature branch (`git checkout -b my-new-feature`)
224
199
  * Add tests for it
225
- * Run `rake mutant` or `rake exhort` to ensure 100% [mutant][mutant] coverage
226
200
  * Commit your changes (`git commit -am '[UPDATE] Add some feature'`)
227
201
  * Push to the branch (`git push origin my-new-feature`)
228
202
  * Create a new Pull Request
@@ -2,7 +2,6 @@ $LOAD_PATH.push File.expand_path("../lib", __FILE__)
2
2
  require "attributes_dsl/version"
3
3
 
4
4
  Gem::Specification.new do |gem|
5
-
6
5
  gem.name = "attributes_dsl"
7
6
  gem.version = AttributesDSL::VERSION.dup
8
7
  gem.author = "Andrew Kozin"
@@ -16,11 +15,11 @@ Gem::Specification.new do |gem|
16
15
  gem.extra_rdoc_files = Dir["README.md", "LICENSE"]
17
16
  gem.require_paths = ["lib"]
18
17
 
19
- gem.required_ruby_version = ">= 1.9.3"
20
-
21
- gem.add_runtime_dependency "ice_nine", "~> 0.11"
22
- gem.add_runtime_dependency "equalizer", "~> 0.0", ">= 0.0.11"
18
+ gem.required_ruby_version = ">= 2.1"
23
19
 
24
- gem.add_development_dependency "hexx-rspec", "~> 0.5", ">= 0.5.2"
20
+ gem.add_runtime_dependency "equalizer", "~> 0.0.11"
21
+ gem.add_runtime_dependency "transproc", "~> 0.4.0"
25
22
 
26
- end # Gem::Specification
23
+ gem.add_development_dependency "hexx-rspec", "~> 0.5.2"
24
+ gem.add_development_dependency "ice_nine", "~> 0.11.1"
25
+ end
data/benchmark/run.rb CHANGED
@@ -6,10 +6,10 @@ module AttributesDSLExample
6
6
  class User
7
7
  extend AttributesDSL
8
8
 
9
- attribute :foo, required: true, &:to_s
10
- attribute :bar, default: :BAR, &:to_s
11
- attribute :baz, default: :BAZ, &:to_s
12
- attribute :qux, &:to_s
9
+ attribute :foo, required: true
10
+ attribute :bar, default: :BAR
11
+ attribute :baz, default: :BAZ
12
+ attribute :qux
13
13
  end
14
14
 
15
15
  def self.call
@@ -1,7 +1,7 @@
1
1
  # encoding: utf-8
2
2
 
3
3
  require "equalizer"
4
- require "ice_nine"
4
+ require "transproc"
5
5
 
6
6
  # Simple DSL for PORO attributes
7
7
  #
@@ -11,6 +11,7 @@ require "ice_nine"
11
11
  #
12
12
  module AttributesDSL
13
13
 
14
+ require_relative "attributes_dsl/transprocs"
14
15
  require_relative "attributes_dsl/attribute"
15
16
  require_relative "attributes_dsl/attributes"
16
17
 
@@ -31,35 +32,26 @@ module AttributesDSL
31
32
  # extend AttributeDSL
32
33
  #
33
34
  # attribute :foo, required: true do |value|
34
- # value.to_i % 5
35
+ # value.to_i % 5 # value coercer
35
36
  # end
36
37
  #
37
- # attribute :bar, default: :BAR
38
+ # attribute :bar, default: :BAR, reader: false
38
39
  # end
39
40
  #
40
41
  # @param [#to_sym] name The unique name of the attribute
41
- # @param [Proc] coercer The proc to coerce values (including the default ones)
42
42
  # @param [Hash] options
43
- #
44
- # @option options [Boolean] :required
45
- # Whether the attribute should be required by the +initializer+
46
- # This option is ignored (set to +false+) when default value is provided
47
- # @option options [Object] :default
48
- # The default value for the attribute
43
+ # @param [Proc] coercer The proc to coerce values (including the default ones)
49
44
  #
50
45
  # @return [undefined]
51
46
  #
52
47
  def attribute(name, options = {}, &coercer)
53
- s_name = name.to_sym
54
- @attributes = attributes.register(s_name, options, &coercer)
55
-
56
- define_method(s_name) { attributes.fetch(s_name) }
48
+ @attributes = attributes.add(name, options, &coercer)
49
+ define_method(name) { attributes.fetch(name) } if attributes.reader? name
57
50
  end
58
51
 
59
52
  # @private
60
53
  def self.extended(klass)
61
- # use __send__ for compatibility to 1.9.3 (where `.include` was private)
62
- klass.__send__(:include, InstanceMethods)
54
+ klass.instance_eval { include InstanceMethods }
63
55
  end
64
56
 
65
57
  # @private
@@ -69,7 +61,6 @@ module AttributesDSL
69
61
 
70
62
  # Defines instance methods for the hash of attributes and its initializer
71
63
  module InstanceMethods
72
-
73
64
  # @!attribute [r] attributes
74
65
  #
75
66
  # @return [Hash] the hash of initialized attributes
@@ -80,12 +71,8 @@ module AttributesDSL
80
71
  #
81
72
  # @param [Hash] attributes
82
73
  #
83
- # @raise [ArgumentError] in case a required attribute is missed
84
- #
85
74
  def initialize(attributes = {})
86
- @attributes = self.class.attributes.extract(attributes)
75
+ @attributes = self.class.attributes.transformer[attributes]
87
76
  end
88
-
89
- end # module InstanceMethods
90
-
91
- end # module AttributesDSL
77
+ end
78
+ end
@@ -9,7 +9,6 @@ module AttributesDSL
9
9
  # @author Andrew Kozin <Andrew.Kozin@gmail.com>
10
10
  #
11
11
  class Attribute
12
-
13
12
  include Equalizer.new(:name)
14
13
 
15
14
  # @!attribute [r] name
@@ -18,52 +17,68 @@ module AttributesDSL
18
17
  #
19
18
  attr_reader :name
20
19
 
21
- # @!attribute [r] default
22
- #
23
- # @return [Object] the default value of the attribute
24
- #
25
- attr_reader :default
26
-
27
- # @!attribute [r] required
28
- #
29
- # @return [Boolean] whether the attribute is required
30
- #
31
- attr_reader :required
32
-
33
- # @!attribute [r] coercer
20
+ # @!attribute [r] reader
34
21
  #
35
- # @return [Proc, nil] the coercer for the attribute
22
+ # @return [Boolean] whether an attribute should be readable
36
23
  #
37
- attr_reader :coercer
24
+ attr_reader :reader
38
25
 
39
26
  # Initializes the attribute
40
27
  #
41
- # @param [Symbol] name
28
+ # @param [#to_sym] name
42
29
  # @param [Hash] options
43
30
  # @param [Proc] coercer
44
31
  #
45
- # @option options [Object] :default
46
- # @option options [Boolean] :required
47
- #
48
32
  def initialize(name, options = {}, &coercer)
49
- @name = name
50
- @default = options.fetch(:default) {}
51
- @required = default.nil? && options.fetch(:required) { false }
52
- @coercer = coercer
53
-
54
- IceNine.deep_freeze(self)
33
+ @name = name.to_sym
34
+ @options = { coercer: coercer }.merge(options)
35
+ @reader = @options.fetch(:reader) { true }
55
36
  end
56
37
 
57
- # Coerces an input assigned to the attribute
38
+ # A proc that transform a hash of attributes using current settings
58
39
  #
59
- # @param [Object] input
40
+ # @return [Proc]
60
41
  #
61
- # @return [Object]
62
- #
63
- def value(input)
64
- coercer ? coercer[input] : input
42
+ def transformer
43
+ convert unless @options.empty?
65
44
  end
66
45
 
67
- end # class Attribute
46
+ private
47
+
48
+ def convert
49
+ @convert ||= Transprocs[:convert, name, presence, absence]
50
+ end
51
+
52
+ def presence
53
+ [whitelist, blacklist, coercer].compact.reduce(:>>) || identity
54
+ end
68
55
 
69
- end # module AttributesDSL
56
+ def absence
57
+ [missed, default].compact.reduce(:>>) || identity
58
+ end
59
+
60
+ def identity
61
+ Transprocs[:identity]
62
+ end
63
+
64
+ def missed
65
+ Transprocs[:missed, name] if @options[:required]
66
+ end
67
+
68
+ def default
69
+ Transprocs[:default, name, @options[:default]] if @options[:default]
70
+ end
71
+
72
+ def whitelist
73
+ Transprocs[:whitelist, name, @options[:only]] if @options[:only]
74
+ end
75
+
76
+ def blacklist
77
+ Transprocs[:blacklist, name, @options[:except]] if @options[:except]
78
+ end
79
+
80
+ def coercer
81
+ Transprocs[:coerce, name, @options[:coercer]] if @options[:coercer]
82
+ end
83
+ end
84
+ end
@@ -10,22 +10,14 @@ module AttributesDSL
10
10
  # @author Andrew Kozin <Andrew.Kozin@gmail.com>
11
11
  #
12
12
  class Attributes
13
-
14
13
  # @!attribute [r] attributes
15
14
  #
16
15
  # Uses the set of attributes to ensure their uniqueness (by name)
17
16
  #
18
17
  # @return [Set] the set of registered attributes
19
18
  #
20
- attr_reader :attributes
21
-
22
- # Initializes an immutable collection with an initial set of attributes
23
- #
24
- # @param [Hash] attributes
25
- #
26
- def initialize(attributes = {})
27
- @attributes = attributes
28
- IceNine.deep_freeze(self)
19
+ def attributes
20
+ @attributes ||= {}
29
21
  end
30
22
 
31
23
  # Initializes the attribute from given arguments
@@ -35,41 +27,49 @@ module AttributesDSL
35
27
  #
36
28
  # @return [AttributesDSL::Attributes]
37
29
  #
38
- def register(name, options = {}, &coercer)
39
- self.class.new(
40
- attributes.merge(name => Attribute.new(name, options, &coercer))
41
- )
30
+ def add(name, options = {}, &coercer)
31
+ name = name.to_sym
32
+ value = Attribute.new(name, options, &coercer)
33
+ clone_with do
34
+ @attributes = attributes.merge(name => value)
35
+ @transformer = nil
36
+ end
42
37
  end
43
38
 
44
- # Extracts instance attributes from the input hash
39
+ # Returns the proc that converts a hash of attributes using current setting
40
+ #
41
+ # @return [Proc]
45
42
  #
46
- # Assigns default values and uses coercions when applicable.
43
+ def transformer
44
+ @transformer ||= transprocs.flatten.compact.reduce(:>>)
45
+ end
46
+
47
+ # Checks whether an attribute reader should be defined
47
48
  #
48
- # @param [Hash] input
49
+ # @param [#to_sym] name
49
50
  #
50
- # @return [Hash]
51
+ # @return [Boolean]
51
52
  #
52
- def extract(input)
53
- validate(input).inject({}) do |a, e|
54
- key = e.name
55
- value = input.fetch(key) { e.default }
56
- a.merge(key => e.value(value))
57
- end
53
+ def reader?(name)
54
+ attributes[name.to_sym].reader
58
55
  end
59
56
 
60
57
  private
61
58
 
62
- def validate(input)
63
- undefined = required - input.keys
64
- return attributes.values if undefined.empty?
65
-
66
- fail ArgumentError.new "Undefined attributes: #{undefined.join(", ")}"
59
+ def transprocs
60
+ [
61
+ Transprocs[:filter, keys],
62
+ attributes.values.map(&:transformer),
63
+ Transprocs[:update, keys]
64
+ ]
67
65
  end
68
66
 
69
- def required
70
- attributes.values.select(&:required).map(&:name)
67
+ def keys
68
+ attributes.keys
71
69
  end
72
70
 
73
- end # class Attributes
74
-
75
- end # module AttributesDSL
71
+ def clone_with(&block)
72
+ dup.tap { |instance| instance.instance_eval(&block) }
73
+ end
74
+ end
75
+ end