attribution 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ *.swp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in attribution.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Paul Barry
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,127 @@
1
+ # Attribution
2
+
3
+ Attribution is a gem to allow you to define attributes for a Ruby object so that getters and setters will be defined that handle typecasting. It also allows you to define associations between objects in an [ActiveRecord-style way][ar].
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'attribution'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install attribution
18
+
19
+ ## Usage
20
+
21
+ You can define attributes like this:
22
+
23
+ class Book
24
+ include Attribution
25
+
26
+ integer :id
27
+ string :title
28
+ decimal :price
29
+ date :published_on
30
+ boolean :ebook_available
31
+ boolean :used
32
+ float :shipping_weight
33
+ time :created_at
34
+ time :updated_at
35
+ time_zone :time_zone
36
+
37
+ has_many :chapters
38
+ end
39
+
40
+ class Chapter
41
+ include Attribution
42
+
43
+ integer :number
44
+ string :title
45
+ integer :page_number
46
+
47
+ belongs_to :book
48
+ end
49
+
50
+ And then you can pass in a Hash or a String of JSON to initialize the object:
51
+
52
+ json = %{{
53
+ "id": 1,
54
+ "title": "Rework",
55
+ "price": "22.00",
56
+ "published_on": "March 9, 2010",
57
+ "ebook_available": "yes",
58
+ "used": "no",
59
+ "shipping_weight": "14.4",
60
+ "created_at": "2013-02-20 05:39:45 -0500",
61
+ "updated_at": "2013-02-20T05:40:37-05:00",
62
+ "time_zone": "Eastern Time (US & Canada)",
63
+ "chapters": [
64
+ {
65
+ "number": "1",
66
+ "title": "Introduction",
67
+ "page_number": "1"
68
+ },
69
+ {
70
+ "number": "2",
71
+ "title": "Takedowns",
72
+ "page_number": "7"
73
+ },
74
+ {
75
+ "number": "3",
76
+ "title": "Go",
77
+ "page_number": "29"
78
+ }
79
+ ]
80
+ }}
81
+
82
+ book = Book.new(json)
83
+
84
+ The object is populated based on the data, the values are converted into the type defined by the attribute:
85
+
86
+ >> book.id
87
+ => 1
88
+ >> book.title
89
+ => "Rework"
90
+ >> book.price
91
+ => #<BigDecimal:7f82dfe9d018,'0.22E2',9(18)>
92
+ >> book.published_on
93
+ => Tue, 09 Mar 2010
94
+ >> book.ebook_available?
95
+ => true
96
+ >> book.used?
97
+ => false
98
+ >> book.shipping_weight
99
+ => 14.4
100
+ >> book.created_at
101
+ => 2013-02-20 05:39:45 -0500
102
+ >> book.updated_at
103
+ => 2013-02-20 05:40:37 -0500
104
+ >> book.time_zone
105
+ => GMT-05:00 Eastern Time US Canada
106
+
107
+ Also, the association is populated with an array of objects:
108
+
109
+ >> book.chapters.size
110
+ => 3
111
+ >> book.chapters[2].page_number
112
+ => 29
113
+
114
+ The reciprocating association is populated as well:
115
+
116
+ >> book.chapters[2].book.title
117
+ => "Rework"
118
+
119
+ ## Contributing
120
+
121
+ 1. Fork it
122
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
123
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
124
+ 4. Push to the branch (`git push origin my-new-feature`)
125
+ 5. Create new Pull Request
126
+
127
+ [ar]: http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,19 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ Gem::Specification.new do |gem|
4
+ gem.name = "attribution"
5
+ gem.version = "0.0.1"
6
+ gem.authors = ["Paul Barry"]
7
+ gem.email = ["mail@paulbarry.com"]
8
+ gem.description = %q{Add attributes to Ruby objects}
9
+ gem.summary = %q{Add attributes to Ruby objects}
10
+ gem.homepage = ""
11
+
12
+ gem.files = `git ls-files`.split($/)
13
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
14
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
15
+ gem.require_paths = ["lib"]
16
+
17
+ gem.add_runtime_dependency "activesupport"
18
+ gem.add_runtime_dependency "tzinfo"
19
+ end
@@ -0,0 +1,3 @@
1
+ module Attribution
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,197 @@
1
+ require "attribution/version"
2
+ require "active_support/core_ext"
3
+
4
+ module Attribution
5
+ BOOLEAN_TRUE_STRINGS = ['y','yes','t','true']
6
+
7
+ def self.included(cls)
8
+ cls.extend(ClassMethods)
9
+ end
10
+
11
+ def initialize(attrs={})
12
+ attrs = JSON.parse(attrs) if attrs.is_a?(String)
13
+ attrs.each do |k,v|
14
+ send("#{k}=", v)
15
+ end
16
+ end
17
+
18
+ module ClassMethods
19
+ def cast(obj)
20
+ case obj
21
+ when Hash then new(obj)
22
+ when self then obj
23
+ else raise ArgumentError.new("can't convert #{obj.class} to #{name}")
24
+ end
25
+ end
26
+
27
+ # Attribute macros
28
+ def string(attr)
29
+ attr_reader(attr)
30
+ define_method("#{attr}=") do |arg|
31
+ instance_variable_set("@#{attr}", arg.to_s)
32
+ end
33
+ end
34
+
35
+ def boolean(attr)
36
+ attr_reader(attr)
37
+ define_method("#{attr}=") do |arg|
38
+ v = case arg
39
+ when String then BOOLEAN_TRUE_STRINGS.include?(arg.downcase)
40
+ when Numeric then arg == 1
41
+ else !!arg
42
+ end
43
+ instance_variable_set("@#{attr}", v)
44
+ end
45
+ alias_method "#{attr}?", attr
46
+ end
47
+
48
+ def integer(attr)
49
+ attr_reader(attr)
50
+ define_method("#{attr}=") do |arg|
51
+ instance_variable_set("@#{attr}", arg.to_i)
52
+ end
53
+ end
54
+
55
+ def float(attr)
56
+ attr_reader(attr)
57
+ define_method("#{attr}=") do |arg|
58
+ instance_variable_set("@#{attr}", arg.to_f)
59
+ end
60
+ end
61
+
62
+ def decimal(attr)
63
+ attr_reader(attr)
64
+ define_method("#{attr}=") do |arg|
65
+ instance_variable_set("@#{attr}", BigDecimal.new(arg.to_s))
66
+ end
67
+ end
68
+
69
+ def date(attr)
70
+ attr_reader(attr)
71
+ define_method("#{attr}=") do |arg|
72
+ v = case arg
73
+ when Date then arg
74
+ when Time, DateTime then arg.to_date
75
+ when String then Date.parse(arg)
76
+ else raise ArgumentError.new("can't convert #{arg.class} to Date")
77
+ end
78
+ instance_variable_set("@#{attr}", v)
79
+ end
80
+ end
81
+
82
+ def time(attr)
83
+ attr_reader(attr)
84
+ define_method("#{attr}=") do |arg|
85
+ v = case arg
86
+ when Date, DateTime then arg.to_time
87
+ when Time then arg
88
+ when String then Time.parse(arg)
89
+ else raise ArgumentError.new("can't convert #{arg.class} to Time")
90
+ end
91
+ instance_variable_set("@#{attr}", v)
92
+ end
93
+ end
94
+
95
+ def time_zone(attr)
96
+ attr_reader(attr)
97
+ define_method("#{attr}=") do |arg|
98
+ instance_variable_set("@#{attr}", ActiveSupport::TimeZone[arg.to_s])
99
+ end
100
+ end
101
+
102
+ # Association macros
103
+ def belongs_to(association_name)
104
+ # foo_id
105
+ define_method("#{association_name}_id") do
106
+ ivar = "@#{association_name}_id"
107
+ if instance_variable_defined?(ivar)
108
+ instance_variable_get(ivar)
109
+ else
110
+ if obj = send(association_name)
111
+ instance_variable_set(ivar, obj.id)
112
+ else
113
+ instance_variable_set(ivar, nil)
114
+ end
115
+ end
116
+ end
117
+
118
+ # foo_id=
119
+ define_method("#{association_name}_id=") do |arg|
120
+ instance_variable_set("@#{association_name}_id", arg.to_i)
121
+ end
122
+
123
+ # foo
124
+ define_method(association_name) do
125
+ if instance_variable_defined?("@#{association_name}")
126
+ instance_variable_get("@#{association_name}")
127
+ elsif id = instance_variable_get("@#{association_name}_id")
128
+ # TODO: Support a more generic version of lazy-loading
129
+ begin
130
+ association_class = association_name.to_s.classify.constantize
131
+ rescue NameError => ex
132
+ raise ArgumentError.new("Association #{association_name} in #{self.class} is invalid because #{association_name.to_s.classify} does not exist")
133
+ end
134
+
135
+ if association_class.respond_to?(:find)
136
+ instance_variable_set("@#{association_name}", association_class.find(id))
137
+ end
138
+ else
139
+ instance_variable_set("@#{association_name}", nil)
140
+ end
141
+ end
142
+
143
+ # foo=
144
+ define_method("#{association_name}=") do |arg|
145
+ begin
146
+ association_class = association_name.to_s.classify.constantize
147
+ rescue NameError => ex
148
+ raise ArgumentError.new("Association #{association_name} in #{self.class} is invalid because #{association_name.to_s.classify} does not exist")
149
+ end
150
+
151
+ if instance_variable_defined?("@#{association_name}_id")
152
+ remove_instance_variable("@#{association_name}_id")
153
+ end
154
+ instance_variable_set("@#{association_name}", association_class.cast(arg))
155
+ end
156
+ end
157
+
158
+ def has_many(association_name)
159
+ # foos
160
+ define_method(association_name) do
161
+ ivar = "@#{association_name}"
162
+ if instance_variable_defined?(ivar)
163
+ instance_variable_get(ivar)
164
+ else
165
+ # TODO: Support a more generic version of lazy-loading
166
+ begin
167
+ association_class = association_name.to_s.singularize.classify.constantize
168
+ rescue NameError => ex
169
+ raise ArgumentError.new("Association #{association_name} in #{self.class} is invalid because #{association_name.to_s.classify} does not exist")
170
+ end
171
+
172
+ if association_class.respond_to?(:all)
173
+ instance_variable_set(ivar, Array(association_class.all("#{self.class.name.underscore}_id" => id)))
174
+ end
175
+ end
176
+ end
177
+
178
+ # foos=
179
+ define_method("#{association_name}=") do |arg|
180
+ # TODO: put this in method
181
+ begin
182
+ association_class = association_name.to_s.singularize.classify.constantize
183
+ rescue NameError => ex
184
+ raise ArgumentError.new("Association #{association_name} in #{self.class} is invalid because #{association_name.to_s.classify} does not exist")
185
+ end
186
+
187
+ objs = Array(arg).map do |obj|
188
+ o = association_class.cast(obj)
189
+ o.send("#{self.class.name.underscore}=", self)
190
+ o.send("#{self.class.name.underscore}_id=", id)
191
+ o
192
+ end
193
+ instance_variable_set("@#{association_name}", objs)
194
+ end
195
+ end
196
+ end
197
+ end
@@ -0,0 +1,86 @@
1
+ require 'test/unit'
2
+ require 'attribution'
3
+
4
+ class Book
5
+ include Attribution
6
+
7
+ integer :id
8
+ string :title
9
+ decimal :price
10
+ date :published_on
11
+ boolean :ebook_available
12
+ boolean :used
13
+ float :shipping_weight
14
+ time :created_at
15
+ time :updated_at
16
+ time_zone :time_zone
17
+
18
+ has_many :chapters
19
+ end
20
+
21
+ class Chapter
22
+ include Attribution
23
+
24
+ integer :number
25
+ string :title
26
+ integer :page_number
27
+
28
+ belongs_to :book
29
+ end
30
+
31
+ class AttributionTest < Test::Unit::TestCase
32
+
33
+ def test_create
34
+ data = {
35
+ :id => 1,
36
+ :title => "Rework",
37
+ :price => "22.00",
38
+ :published_on => "March 9, 2010",
39
+ :ebook_available => "yes",
40
+ :used => "no",
41
+ :shipping_weight => "14.4",
42
+ :created_at => "2013-02-20 05:39:45 -0500",
43
+ :updated_at => "2013-02-20T05:40:37-05:00",
44
+ :time_zone => "Eastern Time (US & Canada)",
45
+ :chapters => [
46
+ {
47
+ :number => "1",
48
+ :title => "Introduction",
49
+ :page_number => "1"
50
+ }, {
51
+ :number => "2",
52
+ :title => "Takedowns",
53
+ :page_number => "7"
54
+ }, {
55
+ :number => "3",
56
+ :title => "Go",
57
+ :page_number => "29"
58
+ }
59
+ ]
60
+ }
61
+
62
+ puts JSON.pretty_generate(data)
63
+
64
+ puts data.to_json.inspect
65
+
66
+ book = Book.new(data.to_json)
67
+
68
+ assert_equal 1, book.id
69
+ assert_equal "Rework", book.title
70
+ assert_equal BigDecimal.new("22.00"), book.price
71
+ assert_equal BigDecimal, book.price.class
72
+ assert_equal Date.new(2010, 3, 9), book.published_on
73
+ assert_equal true, book.ebook_available
74
+ assert_equal true, book.ebook_available?
75
+ assert_equal false, book.used
76
+ assert_equal false, book.used?
77
+ assert_equal 14.4, book.shipping_weight
78
+ assert_equal Float, book.shipping_weight.class
79
+ assert_equal Time.parse("2013-02-20T05:39:45-05:00"), book.created_at
80
+ assert_equal Time.parse("2013-02-20T05:40:37-05:00"), book.updated_at
81
+ assert_equal ActiveSupport::TimeZone["Eastern Time (US & Canada)"], book.time_zone
82
+ assert_equal 1, book.chapters.first.number
83
+ assert_equal 3, book.chapters.size
84
+ assert_equal book, book.chapters.first.book
85
+ end
86
+ end
metadata ADDED
@@ -0,0 +1,88 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: attribution
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Paul Barry
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-02-20 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: activesupport
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: tzinfo
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ description: Add attributes to Ruby objects
47
+ email:
48
+ - mail@paulbarry.com
49
+ executables: []
50
+ extensions: []
51
+ extra_rdoc_files: []
52
+ files:
53
+ - .gitignore
54
+ - Gemfile
55
+ - LICENSE.txt
56
+ - README.md
57
+ - Rakefile
58
+ - attribution.gemspec
59
+ - lib/attribution.rb
60
+ - lib/attribution/version.rb
61
+ - test/attribution_test.rb
62
+ homepage: ''
63
+ licenses: []
64
+ post_install_message:
65
+ rdoc_options: []
66
+ require_paths:
67
+ - lib
68
+ required_ruby_version: !ruby/object:Gem::Requirement
69
+ none: false
70
+ requirements:
71
+ - - ! '>='
72
+ - !ruby/object:Gem::Version
73
+ version: '0'
74
+ required_rubygems_version: !ruby/object:Gem::Requirement
75
+ none: false
76
+ requirements:
77
+ - - ! '>='
78
+ - !ruby/object:Gem::Version
79
+ version: '0'
80
+ requirements: []
81
+ rubyforge_project:
82
+ rubygems_version: 1.8.24
83
+ signing_key:
84
+ specification_version: 3
85
+ summary: Add attributes to Ruby objects
86
+ test_files:
87
+ - test/attribution_test.rb
88
+ has_rdoc: