attribs 1.0.0

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: 41d8896fb88e2a974780b53d830f1e1a59f89ec6
4
+ data.tar.gz: ab506e5dd05cf412fe6c2b47117251ef871da5ca
5
+ SHA512:
6
+ metadata.gz: 8c6043299fe57754ebeca285fc1ab41100d3e01805a41126672f2d37151b0cbf3290aaf717a520ebb92e518668585ffacb3a4ea3538cd86a683691fa1ba391d9
7
+ data.tar.gz: 9d6e0984bc0e21bbda2b3555857163762f8d64c42a32e4307432aa3af3c71580fdb233550e7a5de4c352b3067c8efcc25199aa13df1cd4d2f669345ea74d4ce6
data/.gitignore ADDED
@@ -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
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ -r spec_helper
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2015 Arne Brasseur
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.
data/README.md ADDED
@@ -0,0 +1,44 @@
1
+ # Attribs
2
+
3
+ Easy and flexible Ruby value objects.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'attribs'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ gem install -g
16
+
17
+
18
+ Or install it directly:
19
+
20
+ $ gem install attribs
21
+
22
+ ## Usage
23
+
24
+ ``` ruby
25
+ class Widget
26
+ include Attribs.new(:color, :size, quantity: 1)
27
+ end
28
+
29
+ w = Widget.new(color: 'blue', size: '10')
30
+ w2 = w.with(color: 'red')
31
+ puts w2.pp
32
+ w.to_h
33
+ ```
34
+
35
+ ## Shoutout
36
+
37
+ To [Anima](https://github.com/mbj/anima), which powers most of what
38
+ Attribs offers.
39
+
40
+ ## License
41
+
42
+ © 2014-2015 Arne Brasseur
43
+
44
+ MIT License (see LICENSE.txt)
data/Rakefile ADDED
@@ -0,0 +1,31 @@
1
+ require 'rubygems/package_task'
2
+
3
+ spec = Gem::Specification.load(File.expand_path('../attribs.gemspec', __FILE__))
4
+ gem = Gem::PackageTask.new(spec)
5
+ gem.define
6
+
7
+ desc "Push gem to rubygems.org"
8
+ task :push => :gem do
9
+ sh "git tag v#{spec.version}"
10
+ sh "git push --tags"
11
+ sh "gem push pkg/attribs-#{spec.version}.gem"
12
+ end
13
+
14
+ require 'mutant'
15
+ task :default => :mutant
16
+
17
+ desc "run mutant"
18
+ task :mutant do
19
+ pattern = ENV.fetch('PATTERN', 'Attribs*')
20
+ opts = ENV.fetch('MUTANT_OPTS', '').split(' ')
21
+ result = Mutant::CLI.run(%w[-Ilib -rattribs --use rspec --score 100] + opts + [pattern])
22
+ fail unless result == Mutant::CLI::EXIT_SUCCESS
23
+ end
24
+
25
+ require 'rspec/core/rake_task'
26
+
27
+ desc "run rspec"
28
+ RSpec::Core::RakeTask.new(:rspec) do |t, task_args|
29
+ t.rspec_opts = "-Ispec"
30
+ t.pattern = "spec"
31
+ end
data/attribs.gemspec ADDED
@@ -0,0 +1,24 @@
1
+ # coding: utf-8
2
+
3
+ Gem::Specification.new do |gem|
4
+ gem.name = "attribs"
5
+ gem.version = "1.0.0"
6
+ gem.authors = ["Arne Brasseur"]
7
+ gem.email = ["arne@arnebrasseur.net"]
8
+ gem.summary = %q{Perfect Features Toggles}
9
+ gem.description = %q{Multi-strategy feature flags.}
10
+ gem.homepage = "https://github.com/plexus/attribs"
11
+ gem.license = "MIT"
12
+
13
+ gem.require_paths = ["lib"]
14
+ gem.files = `git ls-files -z`.split("\x0")
15
+ gem.executables = gem.files.grep(%r{^bin/}) { |f| File.basename(f) }
16
+ gem.test_files = gem.files.grep(%r{^spec/})
17
+ gem.extra_rdoc_files = %w[README.md]
18
+
19
+ gem.add_runtime_dependency "anima", "~> 0.2.0"
20
+
21
+ gem.add_development_dependency "rake"
22
+ gem.add_development_dependency "rspec", "~> 3.1"
23
+ gem.add_development_dependency "mutant-rspec"
24
+ end
@@ -0,0 +1,54 @@
1
+ class Attribs
2
+ module InstanceMethods
3
+ def initialize(attributes = {})
4
+ super(self.class.attributes.defaults.merge(attributes))
5
+ end
6
+
7
+ def with(attributes)
8
+ self.class.new(to_h.update(attributes))
9
+ end
10
+
11
+ def append_to(type, *objects)
12
+ with(type => instance_variable_get("@#{type}") + objects)
13
+ end
14
+
15
+ def to_h_compact
16
+ defaults = self.class.attributes.defaults
17
+ to_h.reject do |attr, value|
18
+ value.equal?(defaults[attr])
19
+ end
20
+ end
21
+
22
+ def pp
23
+ indent = ->(str) { str.lines.map {|l| " #{l}"}.join }
24
+ format = ->(val) { val.respond_to?(:pp) ? val.pp : val.inspect }
25
+
26
+ values = to_h_compact
27
+
28
+ fmt_attrs = values.map do |attr, value|
29
+ fmt_val = case value
30
+ when Array
31
+ if value.inspect.length < 50
32
+ "[#{value.map(&format).join(", ")}]"
33
+ else
34
+ "[\n#{indent[value.map(&format).join(",\n")]}\n]"
35
+ end
36
+ else
37
+ format[value]
38
+ end
39
+ "#{attr}: #{fmt_val}"
40
+ end
41
+
42
+ fmt_attrs_str = fmt_attrs.join(", ")
43
+
44
+ if fmt_attrs_str.length > 50
45
+ fmt_attrs_str = fmt_attrs.join(",\n")
46
+ end
47
+
48
+ if fmt_attrs_str =~ /\n/
49
+ fmt_attrs_str = "\n#{indent[fmt_attrs_str]}\n"
50
+ end
51
+ "#{self.class.name}.new(#{fmt_attrs_str})"
52
+ end
53
+ end
54
+ end
data/lib/attribs.rb ADDED
@@ -0,0 +1,30 @@
1
+ require 'anima'
2
+
3
+ class Attribs < Module
4
+ attr_reader :defaults, :names
5
+
6
+ def initialize(*attrs)
7
+ @defaults = attrs.last.instance_of?(Hash) ? attrs.pop : {}
8
+ @names = (attrs + @defaults.keys).uniq
9
+ end
10
+
11
+ def add(*attrs)
12
+ defaults = attrs.last.instance_of?(Hash) ? attrs.pop : {}
13
+ self.class.new(*[*(names+attrs), @defaults.merge(defaults)])
14
+ end
15
+
16
+ def remove(*attrs)
17
+ self.class.new(*[*(names-attrs), @defaults.reject {|k| attrs.include?(k) }])
18
+ end
19
+
20
+ def included(descendant)
21
+ descendant.module_exec(self) do |this|
22
+ include InstanceMethods,
23
+ Anima.new(*this.names)
24
+
25
+ define_singleton_method(:attributes) { this }
26
+ end
27
+ end
28
+ end
29
+
30
+ require 'attribs/instance_methods'
@@ -0,0 +1,234 @@
1
+ RSpec.describe Attribs do
2
+ subject { Class.new.instance_exec(args) {|args| include Attribs.new(*args) } }
3
+ let(:args) { [:foo, bar: 3] }
4
+
5
+ let(:attributes_module) do
6
+ subject.included_modules.detect do |mod|
7
+ mod.instance_of?(described_class)
8
+ end
9
+ end
10
+
11
+ describe '#initialize' do
12
+ it 'should store defaults' do
13
+ expect(attributes_module.defaults).to eql(bar: 3)
14
+ end
15
+
16
+ it 'should store attribute names' do
17
+ expect(attributes_module.names).to eql [:foo, :bar]
18
+ end
19
+
20
+ context 'without defaults' do
21
+ let(:args) { [:foo, :bar] }
22
+
23
+ it 'should have no defaults' do
24
+ expect(attributes_module.defaults).to eql({})
25
+ end
26
+ end
27
+
28
+ context 'with a name given both with and without default' do
29
+ let(:args) { [:foo, foo: 3] }
30
+
31
+ it 'should only store the name once' do
32
+ expect(attributes_module.names).to eql([:foo])
33
+ end
34
+ end
35
+ end
36
+
37
+ describe '#included' do
38
+ it 'should have a hash-based constructor' do
39
+ expect(subject.new(foo: 3, bar: 4).bar).to equal 4
40
+ end
41
+
42
+ it 'should have defaults constructor' do
43
+ expect(subject.new(foo: 3).bar).to equal 3
44
+ end
45
+
46
+ it 'should allow updating through with' do
47
+ expect(subject.new(foo: 3).with(foo: 4).to_h).to eql(foo: 4, bar: 3)
48
+ end
49
+
50
+ it 'should add an #append_to method' do
51
+ expect(subject.new(foo: [6]).append_to(:foo, 7, 8).foo).to eql [6, 7, 8]
52
+ end
53
+
54
+ context 'with all defaults' do
55
+ subject { Class.new { include Attribs.new(foo: 5, bar: 3) } }
56
+
57
+ it 'should be able to construct without arguments' do
58
+ expect(subject.new.to_h).to eql(foo: 5, bar: 3)
59
+ end
60
+ end
61
+
62
+ context 'without any defaults' do
63
+ subject { Class.new { include Attribs.new(:foo, :bar) } }
64
+
65
+ it 'should allow setting all attributes' do
66
+ expect(subject.new(foo: 5, bar: 6).bar).to equal 6
67
+ end
68
+
69
+ it 'should expect all attributes' do
70
+ expect { subject.new(foo: 5) }.to raise_exception
71
+ end
72
+ end
73
+ end
74
+
75
+ describe '#add' do
76
+ subject { Class.new(super()) { include attributes.add(baz: 7, bar: 4) } }
77
+
78
+ it 'should make the new attributes available' do
79
+ expect(subject.new(foo: 3, baz: 6).baz).to equal 6
80
+ end
81
+
82
+ it 'should make the old attributes available' do
83
+ expect(subject.new(foo: 3, baz: 6).foo).to equal 3
84
+ end
85
+
86
+ it 'should take new default values' do
87
+ expect(subject.new(foo: 3, baz: 6).bar).to equal 4
88
+ end
89
+
90
+ it 'should make sure attribute names are uniq' do
91
+ expect(subject.attributes.names.length).to equal 3
92
+ end
93
+
94
+ context 'without any defaults' do
95
+ subject { Class.new(super()) { include attributes.add(:bax) } }
96
+
97
+ it 'should allow setting all attributes' do
98
+ expect(subject.new(foo: 5, bar: 6, bax: 7).bax).to equal 7
99
+ end
100
+
101
+ it 'should expect all attributes' do
102
+ expect { subject.new(foo: 5, bar: 6) }.to raise_exception
103
+ end
104
+ end
105
+ end
106
+
107
+ describe '#remove' do
108
+ context 'when removing an attribute with a default' do
109
+ subject { Class.new(super()) { include attributes.remove(:bar) } }
110
+
111
+ it 'should still recognize attributes that were kept' do
112
+ expect(subject.new(foo: 2).foo).to equal 2
113
+ end
114
+
115
+ it 'should no longer recognize the old attributes' do
116
+ expect { subject.new(foo: 3, bar: 3).bar }.to raise_error
117
+ end
118
+ end
119
+
120
+ context 'when removing an attribute without a default' do
121
+ subject { Class.new(super()) { include attributes.remove(:foo) } }
122
+
123
+ it 'should still recognize attributes that were kept' do
124
+ expect(subject.new(bar: 2).bar).to equal 2
125
+ end
126
+
127
+ it 'should no longer recognize the old attributes' do
128
+ expect { subject.new(foo: 3).foo }.to raise_error
129
+ end
130
+
131
+ it 'should keep the defaults' do
132
+ expect(subject.new.bar).to equal 3
133
+ end
134
+ end
135
+ end
136
+ end
137
+
138
+ RSpec.describe Attribs::InstanceMethods do
139
+ let(:widget) do
140
+ Class.new do
141
+ include Attribs.new(:color, :size, options: {})
142
+ def self.name ; 'Widget' ; end
143
+ end
144
+ end
145
+
146
+ let(:widget_container) do
147
+ Class.new do
148
+ include Attribs.new(widgets: [])
149
+ def self.name ; 'WidgetContainer' ; end
150
+ end
151
+ end
152
+
153
+ let(:fixed_width) do
154
+ Class.new do
155
+ def initialize(width)
156
+ @width = width
157
+ end
158
+
159
+ def inspect
160
+ "#" * @width
161
+ end
162
+ end
163
+ end
164
+
165
+ describe '#pp' do
166
+ it 'should render correctly' do
167
+ expect(widget_container.new(widgets: [
168
+ widget.new(color: :green, size: 7),
169
+ widget.new(color: :blue, size: 9, options: {foo: :bar})
170
+ ]).pp).to eql "
171
+ WidgetContainer.new(
172
+ widgets: [
173
+ Widget.new(color: :green, size: 7),
174
+ Widget.new(color: :blue, size: 9, options: {:foo=>:bar})
175
+ ]
176
+ )
177
+ ".strip
178
+ end
179
+
180
+ it 'should inline short arrays' do
181
+ expect(widget_container.new(widgets: [
182
+ fixed_width.new(23),
183
+ fixed_width.new(22)
184
+ ]).pp).to eql "WidgetContainer.new(widgets: [#######################, ######################])"
185
+ end
186
+
187
+ it 'should put longer arrays on multiple lines' do
188
+ expect(widget_container.new(widgets: [
189
+ fixed_width.new(23),
190
+ fixed_width.new(23)
191
+ ]).pp).to eql "WidgetContainer.new(\n widgets: [\n #######################,\n #######################\n ]\n)"
192
+ end
193
+
194
+ it 'should puts attributes on multiple lines if total length exceeds 50 chars' do
195
+ expect(widget.new(color: fixed_width.new(18), size: fixed_width.new(18)).pp).to match /\n/
196
+ expect(widget.new(color: fixed_width.new(18), size: fixed_width.new(17)).pp).to_not match /\n/
197
+ end
198
+ end
199
+
200
+ describe '#append_to' do
201
+ it 'should append to a named collection' do
202
+ expect(widget_container.new(widgets: [:bar]).append_to(:widgets, :foo)).to eql widget_container.new(widgets: [:bar, :foo])
203
+ end
204
+ end
205
+
206
+ describe '#initialize' do
207
+ it 'should take hash-based args' do
208
+ expect(widget_container.new(widgets: [:bar])).to eql widget_container.new.with(widgets: [:bar])
209
+ end
210
+
211
+ it 'should use defaults when available' do
212
+ expect(widget.new(color: :blue, size: 3).options).to eql({})
213
+ end
214
+ end
215
+
216
+ describe '#to_h_compact' do
217
+ it 'should not show values that are identical to the defaults' do
218
+ expect(widget.new(color: :red, size: 7).to_h_compact)
219
+ .to eql({color: :red, size: 7})
220
+ end
221
+
222
+ it 'should include values that are equivalent but not identical' do
223
+ expect(widget.new(color: :red, size: 7, options: {}).to_h_compact)
224
+ .to eql({color: :red, size: 7, options: {}})
225
+ end
226
+ end
227
+
228
+ describe '#with' do
229
+ it 'should update specific attributes' do
230
+ expect(widget.new(color: :red, size: 7).with(color: :blue))
231
+ .to eql(widget.new(color: :blue, size: 7))
232
+ end
233
+ end
234
+ end
@@ -0,0 +1,9 @@
1
+ require 'attribs'
2
+
3
+ RSpec.configure do |rspec|
4
+ rspec.disable_monkey_patching!
5
+ rspec.raise_errors_for_deprecations!
6
+ rspec.around(:each) do |example|
7
+ Timeout.timeout(1, &example)
8
+ end
9
+ end
metadata ADDED
@@ -0,0 +1,114 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: attribs
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Arne Brasseur
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-04-01 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: anima
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 0.2.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 0.2.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '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.1'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.1'
55
+ - !ruby/object:Gem::Dependency
56
+ name: mutant-rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ description: Multi-strategy feature flags.
70
+ email:
71
+ - arne@arnebrasseur.net
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files:
75
+ - README.md
76
+ files:
77
+ - ".gitignore"
78
+ - ".rspec"
79
+ - Gemfile
80
+ - LICENSE.txt
81
+ - README.md
82
+ - Rakefile
83
+ - attribs.gemspec
84
+ - lib/attribs.rb
85
+ - lib/attribs/instance_methods.rb
86
+ - spec/attribs_spec.rb
87
+ - spec/spec_helper.rb
88
+ homepage: https://github.com/plexus/attribs
89
+ licenses:
90
+ - MIT
91
+ metadata: {}
92
+ post_install_message:
93
+ rdoc_options: []
94
+ require_paths:
95
+ - lib
96
+ required_ruby_version: !ruby/object:Gem::Requirement
97
+ requirements:
98
+ - - ">="
99
+ - !ruby/object:Gem::Version
100
+ version: '0'
101
+ required_rubygems_version: !ruby/object:Gem::Requirement
102
+ requirements:
103
+ - - ">="
104
+ - !ruby/object:Gem::Version
105
+ version: '0'
106
+ requirements: []
107
+ rubyforge_project:
108
+ rubygems_version: 2.4.6
109
+ signing_key:
110
+ specification_version: 4
111
+ summary: Perfect Features Toggles
112
+ test_files:
113
+ - spec/attribs_spec.rb
114
+ - spec/spec_helper.rb