output_attributes 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 5853821c644c05df3134cba84d1fb800540eafe828ee11cbb6e17f4bb1d80fcd
4
+ data.tar.gz: 5b9acc26991eb9f52a1e93633badb9761564b9393cfa1a5a713f453f15c45e30
5
+ SHA512:
6
+ metadata.gz: 0cd70015e649d210c073ee061b6691823642c6ac5dcb7e03ed84ca5089759f56d9b67ac690223d5e48b94089ffcb5fb9f81daffa8d0c920e25606c0d2105bd2e
7
+ data.tar.gz: 30cc7fd8b45ea679001cf3e32ad977222ce0643109643b0c021ef4a19b1f5ed577bfbe3a01d84f49d34d5ae0ef66a9e6c7a7a2808b11a1b88a6fd12779b9756a
data/.gitignore ADDED
@@ -0,0 +1,8 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "https://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in output_attributes.gemspec
4
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,28 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ output_attributes (0.1.0)
5
+
6
+ GEM
7
+ remote: https://rubygems.org/
8
+ specs:
9
+ coderay (1.1.2)
10
+ method_source (0.9.2)
11
+ minitest (5.13.0)
12
+ pry (0.12.2)
13
+ coderay (~> 1.1.0)
14
+ method_source (~> 0.9.0)
15
+ rake (10.5.0)
16
+
17
+ PLATFORMS
18
+ ruby
19
+
20
+ DEPENDENCIES
21
+ bundler (~> 2.0)
22
+ minitest (~> 5.0)
23
+ output_attributes!
24
+ pry
25
+ rake (~> 10.0)
26
+
27
+ BUNDLED WITH
28
+ 2.0.2
data/README.md ADDED
@@ -0,0 +1,214 @@
1
+ # OutputAttributes
2
+
3
+ This gem provides a class macro that adds `output` helpers when defining your class. I find it jarring to keep `#to_hash` up to date on classes that have many data attributes, and a few helper methods. I often wish to just mark a method as "This method describes my data and should be part of `#to_hash`".
4
+
5
+ Behold:
6
+
7
+ ```ruby
8
+ class Item
9
+ include OutputAttributes
10
+
11
+ def name
12
+ "The Name"
13
+ end
14
+ output :name
15
+
16
+ end
17
+
18
+ Item.new.output_attributes
19
+ # => {:name=>"The Name"}
20
+ ```
21
+
22
+ The `output` declaration can come before, during, or after a method definition.
23
+
24
+ ```ruby
25
+ class Item
26
+ include OutputAttributes
27
+
28
+ # Before:
29
+ output :first_name
30
+
31
+ def first_name
32
+ "First Name"
33
+ end
34
+
35
+ # During -- this is my favorite. It leverages the fact that the `def meth` expression returns a symbol... Clever!
36
+ output def middle_name
37
+ "Middle Name"
38
+ end
39
+
40
+ # After
41
+ def last_name
42
+ "Last Name"
43
+ end
44
+
45
+ output :last_name
46
+ end
47
+
48
+ Item.new.output_attributes
49
+ # => {:first_name=>"First Name",
50
+ # :middle_name=>"Middle Name",
51
+ # :last_name=>"Last Name"}
52
+ ```
53
+
54
+ Whatever style works best for you. Stack a bunch on top like typical `attr_readers`. Stash them on the bottom. Decorate them. It's all good.
55
+
56
+ Sometimes the method name is not what you want as your output key. `output` takes an optional `from: ` keyword argument. If `from` is a `Symbol`, it will call that method instead:
57
+
58
+ ```ruby
59
+ class Item
60
+ include OutputAttributes
61
+ def name
62
+ "An Item"
63
+ end
64
+
65
+ output :description, from: :name
66
+ end
67
+
68
+ Item.new.output_attributes
69
+ # => {:description=>'An Item'}
70
+ ```
71
+
72
+ You can also pass a proc or lambda in. The first argument provided to the proc is the instance of the object. This can be helpful if you need just a little extra massaging.
73
+
74
+ ```ruby
75
+ class Item
76
+ include OutputAttributes
77
+ def name
78
+ "An Item"
79
+ end
80
+
81
+ def color
82
+ "Red"
83
+ end
84
+
85
+ output :description, from: ->(item) { [item.name, item.color].join(', ') }
86
+ end
87
+
88
+ Item.new.output_attributes
89
+ # => {:description=>"An Item, Red"}
90
+
91
+ ```
92
+
93
+ You can of course just ignore it as well.
94
+
95
+ ```ruby
96
+ class Item
97
+ include OutputAttributes
98
+
99
+ output :extracted_at, from: ->(_) { Time.now }
100
+ end
101
+
102
+ Item.new.output_attributes
103
+ # => {:extracted_at=>2019-11-26 16:12:01 -0600}
104
+
105
+ ```
106
+
107
+ I don't overwrite `#to_hash` or `#to_h` because I think those methods are kind of special. However, it's incredibly easy to do it yourself!
108
+
109
+ ```ruby
110
+ class Item
111
+ include OutputAttributes
112
+ output def name
113
+ "An Item"
114
+ end
115
+
116
+ alias to_h output_attributes
117
+
118
+ # or
119
+
120
+ def to_hash
121
+ output_attributes.merge(
122
+ with: :more,
123
+ customization: :perhaps
124
+ )
125
+ end
126
+ end
127
+
128
+ item = Item.new
129
+ # => #<Item:0x000055e0ae92d0a8>
130
+ item.output_attributes
131
+ # => {:name=>"An Item"}
132
+ item.to_h
133
+ # => {:name=>"An Item"}
134
+ item.to_hash
135
+ # => {:name=>"An Item", :with=>:more, :customization=>:perhaps}
136
+ ```
137
+
138
+ I find this style particularly useful when working with Page Objects for data extraction:
139
+
140
+ ```ruby
141
+ class Page < SimpleDelegator
142
+ include OutputAttributes
143
+
144
+ output def name
145
+ at_css('#title').text
146
+ end
147
+
148
+ output def price
149
+ at_css('.price').text
150
+ end
151
+
152
+ output def color
153
+ labels(:color)
154
+ end
155
+
156
+ output def size
157
+ labels(:size)
158
+ end
159
+
160
+ output :extracted_at, from: ->(_){ Time.now }
161
+
162
+ alias to_hash output_attributes
163
+
164
+ private
165
+ def labels(key)
166
+ at_css("li:contains('#{key}')").text
167
+ end
168
+
169
+ end
170
+
171
+ Page.new(nokogirilike).to_hash
172
+ ```
173
+
174
+ Usually when I'm writing a method for a page object, I'm thinking "Is this part of my data output, or is this just a helper method?". I've often forgotten to update `#to_hash` when it lives further away from the method itself. I've also tried other styles that involved packaging my data methods into a module, and then doing something like `Attributes.public_instance_methods.reduce({})...` but I wanted to give this style a spin.
175
+
176
+
177
+ # Fun Fact
178
+
179
+ `def method; ...; end` returns a symbol. I saw a recent post on Reddit comparing Python's method decorators. This led to some example code using the Memoist gem that looked like this:
180
+
181
+ ```ruby
182
+ memoize def my_method
183
+ ...
184
+ end
185
+ ```
186
+
187
+ I think this is pretty cool, and is exactly the type of syntax I wanted when creating data objects.
188
+
189
+
190
+ ## Installation
191
+
192
+ Add this line to your application's Gemfile:
193
+
194
+ ```ruby
195
+ gem 'output_attributes'
196
+ ```
197
+
198
+ And then execute:
199
+
200
+ $ bundle
201
+
202
+ Or install it yourself as:
203
+
204
+ $ gem install output_attributes
205
+
206
+ ## Development
207
+
208
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
209
+
210
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
211
+
212
+ ## Contributing
213
+
214
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/output_attributes.
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "test"
6
+ t.libs << "lib"
7
+ t.test_files = FileList["test/**/*_test.rb"]
8
+ end
9
+
10
+ task :default => :test
data/bin/console ADDED
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "output_attributes"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ require "pry"
11
+ Pry.start
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,3 @@
1
+ module OutputAttributes
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,89 @@
1
+ require "output_attributes/version"
2
+
3
+ # This module creates a class helper method `output` that can be used to create an output configuration
4
+ # The goal is to help assemble a class's hash representation.
5
+ #
6
+ # Each time you call `output` in the class definition, you register a key => proc pair.
7
+ # You can then call `#output_attributes` to get the hash of key => values
8
+ #
9
+ # Example:
10
+ #
11
+ # class Item
12
+ # include OutputAttributes
13
+ #
14
+ # # You can register outputs similar to attr_accessors
15
+ # output :name
16
+ #
17
+ # def name
18
+ # "A Thing"
19
+ # end
20
+ #
21
+ # # Since the `def meth` expression returns a symbol, you can also register it like a decorator.
22
+ # # It returns the symbol so you could keep chaining with other similar tools like memoize
23
+ # output def price
24
+ # "free"
25
+ # end
26
+ #
27
+ # # You can rename a method/key:
28
+ # output :cost, from: :price
29
+ #
30
+ # # You can also define a custom proc if the key doesn't match a method name.
31
+ # # The argument to your proc is the instance itself so you have easy access to it's methods
32
+ # output :description, from: ->(item){ [item.name, item.price].join(': ') }
33
+ #
34
+ # # You can also call whatever you want:
35
+ # output :extracted_at, from: ->(_){ Time.now }
36
+ #
37
+ # def a_helper_method
38
+ # "Ignore this"
39
+ # end
40
+ #
41
+ # # It does not override `#to_h/ash`, but this is easy enough if you wish!
42
+ # def to_h
43
+ # output_attributes
44
+ # end
45
+ # alias to_hash output_attributes
46
+ # end
47
+ #
48
+ # item = Item.new
49
+ # item.output_attributes || item.to_h || item.to_hash
50
+ # # =>
51
+ # {
52
+ # name: "A Thing",
53
+ # price: "Free",
54
+ # description: "A Thing: Free",
55
+ # extracted_at: 2019-11-26 14:33:00.000
56
+ # }
57
+
58
+ module OutputAttributes
59
+ # Register this class's catalog of outputs
60
+ def self.included(base)
61
+ base.class_eval do
62
+ @registered_output_attributes = {}
63
+
64
+ def self.output(key, from: nil)
65
+ @registered_output_attributes[key] = from || key
66
+ key
67
+ end
68
+
69
+ def self.registered_output_attributes
70
+ @registered_output_attributes
71
+ end
72
+ end
73
+ end
74
+
75
+ # Return a hash representing your outputs
76
+ def output_attributes
77
+ self.class.registered_output_attributes.map do |key, meth|
78
+ value = case meth
79
+ when Symbol, String
80
+ self.send(meth.to_sym)
81
+ when Proc
82
+ meth.call(self)
83
+ else
84
+ raise ArgumentError, "Could not determine how to output #{meth} for #{key}."
85
+ end
86
+ [key, value]
87
+ end.to_h
88
+ end
89
+ end
@@ -0,0 +1,37 @@
1
+ lib = File.expand_path("lib", __dir__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+ require "output_attributes/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "output_attributes"
7
+ spec.version = OutputAttributes::VERSION
8
+ spec.authors = ["Tim Tilberg"]
9
+ spec.email = ["ttilberg@gmail.com"]
10
+
11
+ spec.summary = %q{Easily declare a hash to represent your object using `output` attributes}
12
+ spec.description = <<~DESC
13
+ Sometimes defining #to_hash is a drag because the source location of your methods
14
+ is far away from `def to_hash`. This gem gives you a declarative way to build up
15
+ a hash representation of your object as you define your methods.
16
+ DESC
17
+ spec.homepage = "https://www.github.com/ttilberg/output_attributes"
18
+
19
+ spec.metadata["allowed_push_host"] = "https://rubygems.org"
20
+
21
+ spec.metadata["homepage_uri"] = spec.homepage
22
+ spec.metadata["source_code_uri"] = "https://www.github.com/ttilberg/output_attributes"
23
+
24
+ # Specify which files should be added to the gem when it is released.
25
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
26
+ spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
27
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
28
+ end
29
+ spec.bindir = "exe"
30
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
31
+ spec.require_paths = ["lib"]
32
+
33
+ spec.add_development_dependency "bundler", "~> 2.0"
34
+ spec.add_development_dependency "rake", "~> 10.0"
35
+ spec.add_development_dependency "minitest", "~> 5.0"
36
+ spec.add_development_dependency "pry"
37
+ end
metadata ADDED
@@ -0,0 +1,114 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: output_attributes
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Tim Tilberg
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2019-11-26 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: '2.0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '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: '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: minitest
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '5.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '5.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: pry
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: |
70
+ Sometimes defining #to_hash is a drag because the source location of your methods
71
+ is far away from `def to_hash`. This gem gives you a declarative way to build up
72
+ a hash representation of your object as you define your methods.
73
+ email:
74
+ - ttilberg@gmail.com
75
+ executables: []
76
+ extensions: []
77
+ extra_rdoc_files: []
78
+ files:
79
+ - ".gitignore"
80
+ - Gemfile
81
+ - Gemfile.lock
82
+ - README.md
83
+ - Rakefile
84
+ - bin/console
85
+ - bin/setup
86
+ - lib/output_attributes.rb
87
+ - lib/output_attributes/version.rb
88
+ - output_attributes.gemspec
89
+ homepage: https://www.github.com/ttilberg/output_attributes
90
+ licenses: []
91
+ metadata:
92
+ allowed_push_host: https://rubygems.org
93
+ homepage_uri: https://www.github.com/ttilberg/output_attributes
94
+ source_code_uri: https://www.github.com/ttilberg/output_attributes
95
+ post_install_message:
96
+ rdoc_options: []
97
+ require_paths:
98
+ - lib
99
+ required_ruby_version: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ required_rubygems_version: !ruby/object:Gem::Requirement
105
+ requirements:
106
+ - - ">="
107
+ - !ruby/object:Gem::Version
108
+ version: '0'
109
+ requirements: []
110
+ rubygems_version: 3.0.4
111
+ signing_key:
112
+ specification_version: 4
113
+ summary: Easily declare a hash to represent your object using `output` attributes
114
+ test_files: []