class2 0.0.1 → 0.0.2
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 +4 -4
- data/.gitignore +1 -1
- data/Changes +5 -0
- data/README.md +118 -14
- data/class2.gemspec +2 -2
- data/lib/class2.rb +100 -12
- data/lib/class2/version.rb +1 -1
- metadata +7 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 91ca10f950914ea029d15364b4722c064dc5d669
|
4
|
+
data.tar.gz: c1b657103a971487aa414c95c7f789959c1bd5bf
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: fe9ece44a7faecc4b52dd0159c4fd0271ab38c65928d4ccdbdd2960ffb0abe8366951152fe2bc83b5fc3001b97758dcd3c9ae25f1fb19d91f665e459b97da5e7
|
7
|
+
data.tar.gz: 0294c237a509932691f6e4fd8f976b1d3d49aea607e27053e15681b94eb69a7f58e2ac7601bf7283a195e6405721d466a43834ffea5bf57800024b590655ac90
|
data/.gitignore
CHANGED
data/Changes
ADDED
data/README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# Class2
|
2
2
|
|
3
|
-
Easily create hierarchies of classes that support nested attributes, equality, and more.
|
3
|
+
Easily create hierarchies of classes that support nested attributes, type conversion, equality, and more.
|
4
4
|
|
5
5
|
[](https://travis-ci.org/sshaw/class2)
|
6
6
|
|
@@ -24,9 +24,28 @@ This creates 3 classes: `User`, `Address`, and `Country` with the following attr
|
|
24
24
|
* `Address`: city, state, zip, country
|
25
25
|
* `Country`: name, code
|
26
26
|
|
27
|
-
Each of these classes
|
27
|
+
Each of these classes are created with [several additional methods](#methods).
|
28
28
|
|
29
|
-
|
29
|
+
You can also specify types:
|
30
|
+
|
31
|
+
```rb
|
32
|
+
Class2(
|
33
|
+
:user => {
|
34
|
+
:name => String,
|
35
|
+
:age => Fixnum,
|
36
|
+
:addresses => [
|
37
|
+
:city, :state, :zip, # No explicit types for these
|
38
|
+
:country => {
|
39
|
+
:name => String,
|
40
|
+
:code => String
|
41
|
+
}
|
42
|
+
]
|
43
|
+
}
|
44
|
+
)
|
45
|
+
```
|
46
|
+
Attributes without types are treated as is.
|
47
|
+
|
48
|
+
After calling either one of the above you can do following:
|
30
49
|
|
31
50
|
```rb
|
32
51
|
user = User.new(
|
@@ -42,22 +61,68 @@ user = User.new(
|
|
42
61
|
]
|
43
62
|
)
|
44
63
|
|
45
|
-
|
46
|
-
|
47
|
-
|
64
|
+
user.name # "sshaw"
|
65
|
+
user.addresses.size # 3
|
66
|
+
user.addresses.first.city # "LA"
|
67
|
+
user.to_h # {:name => "sshaw", :age => 99, :addresses => [ { ... } ]}
|
48
68
|
|
49
|
-
#
|
69
|
+
# keys can be strings too
|
50
70
|
country = Country.new("name" => "America", "code" => "US")
|
51
71
|
address = Address.new(:city => "Da Bay", :state => "CA", :country => country)
|
52
72
|
user.addresses << address
|
53
73
|
|
54
|
-
|
74
|
+
User.new(:name => "sshaw") == User.new(:name => "sshaw") # true
|
75
|
+
```
|
76
|
+
|
77
|
+
Unknown attributes passed to the constructor are ignored.
|
78
|
+
|
79
|
+
`Class2` can create classes with typed attributes from example hashes.
|
80
|
+
This makes it possible to build classes for things like API responses, using the API response
|
81
|
+
itself -or a slightly modified version, see [Issues](#issues)- as the specification:
|
55
82
|
|
56
|
-
|
57
|
-
|
58
|
-
|
83
|
+
```rb
|
84
|
+
# From JSON.parse
|
85
|
+
# of https://api.github.com/repos/sshaw/selfie_formatter/commits
|
86
|
+
response = [
|
87
|
+
{
|
88
|
+
"sha" => "f52f1ed9144e1f73346176ab79a61af78df1b6bd",
|
89
|
+
"commit" => {
|
90
|
+
"author"=> {
|
91
|
+
"name"=>"sshaw",
|
92
|
+
"email"=>"skye.shaw@gmail.com",
|
93
|
+
"date"=>"2016-06-30T03:51:00Z"
|
94
|
+
}
|
95
|
+
},
|
96
|
+
"comment_count": 0
|
97
|
+
|
98
|
+
# snip full response
|
99
|
+
}
|
100
|
+
]
|
101
|
+
|
102
|
+
Class2(
|
103
|
+
:commit => response.first
|
104
|
+
)
|
105
|
+
|
106
|
+
commit = Commit.new(response.first)
|
107
|
+
commit.author.name # "sshaw"
|
108
|
+
commit.comment_count # 0
|
59
109
|
```
|
60
110
|
|
111
|
+
### Conversions
|
112
|
+
|
113
|
+
You can use any of these classes or their instances in your class definitions:
|
114
|
+
|
115
|
+
* `Array`
|
116
|
+
* `Date`
|
117
|
+
* `DateTime`
|
118
|
+
* `Float`
|
119
|
+
* `Hash`
|
120
|
+
* `Integer`/`Fixnum` - either one will cause a `Fixnum` conversion
|
121
|
+
* `TrueClass`/`FalseClass` - either one will cause a boolean conversion
|
122
|
+
|
123
|
+
Custom conversions are possible, just add the conversion to
|
124
|
+
[`Class2::CONVERSIONS`](https://github.com/sshaw/class2/blob/517239afc76a4d80677e169958a1dc7836726659/lib/class2.rb#L14-L29)
|
125
|
+
|
61
126
|
### Namespaces
|
62
127
|
|
63
128
|
`Class2` can use an exiting namespace or create a new one:
|
@@ -82,9 +147,13 @@ New::Namespace::User.new(:name => "sshaw")
|
|
82
147
|
|
83
148
|
`Class2` uses
|
84
149
|
[`String#classify`](http://api.rubyonrails.org/classes/String.html#method-i-classify)
|
85
|
-
to turn keys into class names
|
86
|
-
be `FooBar`.
|
87
|
-
|
150
|
+
to turn keys into class names: `:foo` will be `Foo`, `:foo_bars` will
|
151
|
+
be `FooBar`.
|
152
|
+
|
153
|
+
Plural keys with an array value are always assumed to be accessors for
|
154
|
+
a collection and will default to returning an `Array`. `#classify` is
|
155
|
+
used to derive the class names from the plural attribute names. An
|
156
|
+
`:addresses` key with an `Array` value will result in a class named
|
88
157
|
`Address` being created.
|
89
158
|
|
90
159
|
Plurality is determined by [`String#pluralize`](http://api.rubyonrails.org/classes/String.html#method-i-pluralize).
|
@@ -115,6 +184,41 @@ end
|
|
115
184
|
User.new(:name => "sshaw").first_initial
|
116
185
|
```
|
117
186
|
|
187
|
+
## Issues
|
188
|
+
|
189
|
+
Can't use plural attributes that are not collections.
|
190
|
+
Here we want `:users` to be an attribute, not a type:
|
191
|
+
|
192
|
+
```rb
|
193
|
+
Class2(:foo => [ :users => [ :id, :age, :foo ] ])
|
194
|
+
```
|
195
|
+
|
196
|
+
Instead you can use a `Set`
|
197
|
+
|
198
|
+
```rb
|
199
|
+
Class2(:foo => [ :users => Set.new([ :id, :age, :foo ]) ])
|
200
|
+
```
|
201
|
+
|
202
|
+
---
|
203
|
+
|
204
|
+
Building classes from example hashes will fail for arrays of scalars. For example:
|
205
|
+
|
206
|
+
```rb
|
207
|
+
user = { :name => "sshaw", :hobbies => ["screen-staring", "other thangz"] }
|
208
|
+
Class2(:user => user)
|
209
|
+
```
|
210
|
+
|
211
|
+
Will fail because of `:hobbies`' value. It must be converted to:
|
212
|
+
|
213
|
+
```rb
|
214
|
+
user = { :name => "sshaw", :hobbies => [] }
|
215
|
+
Class2(:user => user)
|
216
|
+
```
|
217
|
+
|
218
|
+
---
|
219
|
+
|
220
|
+
Others, I'm sure.
|
221
|
+
|
118
222
|
## See Also
|
119
223
|
|
120
224
|
The Perl module that served as the inspiration: [`MooseX::NestedAttributesConstructor`](https://github.com/sshaw/MooseX-NestedAttributesConstructor).
|
data/class2.gemspec
CHANGED
@@ -9,7 +9,7 @@ Gem::Specification.new do |spec|
|
|
9
9
|
spec.authors = ["Skye Shaw"]
|
10
10
|
spec.email = ["skye.shaw@gmail.com"]
|
11
11
|
|
12
|
-
spec.summary = %q{Easily create hierarchies of classes that support nested attributes, equality, and more.}
|
12
|
+
spec.summary = %q{Easily create hierarchies of classes that support nested attributes, type conversion, equality, and more.}
|
13
13
|
spec.homepage = "https://github.com/sshaw/class2"
|
14
14
|
spec.license = "MIT"
|
15
15
|
|
@@ -18,7 +18,7 @@ Gem::Specification.new do |spec|
|
|
18
18
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
19
19
|
spec.require_paths = ["lib"]
|
20
20
|
|
21
|
-
spec.add_dependency "activesupport", ">= 3.
|
21
|
+
spec.add_dependency "activesupport", ">= 3.2"
|
22
22
|
|
23
23
|
spec.add_development_dependency "bundler", "~> 1.10"
|
24
24
|
spec.add_development_dependency "rake", "~> 10.0"
|
data/lib/class2.rb
CHANGED
@@ -1,14 +1,33 @@
|
|
1
1
|
# coding: utf-8
|
2
2
|
|
3
|
-
require "
|
3
|
+
require "date"
|
4
4
|
require "active_support/core_ext/module"
|
5
5
|
require "active_support/core_ext/string"
|
6
6
|
|
7
|
+
require "class2/version"
|
8
|
+
|
7
9
|
def Class2(*args)
|
8
10
|
Class2.new(*args)
|
9
11
|
end
|
10
12
|
|
11
13
|
class Class2
|
14
|
+
CONVERSIONS = {
|
15
|
+
Array => lambda { |v| "Array(#{v})" },
|
16
|
+
Date => lambda { |v| "Date.parse(#{v})" },
|
17
|
+
DateTime => lambda { |v| "DateTime.parse(#{v})" },
|
18
|
+
Float => lambda { |v| "Float(#{v})" },
|
19
|
+
Hash => lambda { |v| sprintf "%s.respond_to?(:to_h) ? %s.to_h : %s", v, v, v },
|
20
|
+
Integer => lambda { |v| "Integer(#{v})" },
|
21
|
+
String => lambda { |v| "String(#{v})" },
|
22
|
+
TrueClass => lambda do |v|
|
23
|
+
sprintf '["1", 1, 1.0, true].freeze.include?(%s.is_a?(String) ? %s.strip : %s)', v, v, v
|
24
|
+
end
|
25
|
+
}
|
26
|
+
|
27
|
+
CONVERSIONS[FalseClass] = CONVERSIONS[TrueClass]
|
28
|
+
CONVERSIONS[Fixnum] = CONVERSIONS[Integer]
|
29
|
+
CONVERSIONS.default = lambda { |v| v }
|
30
|
+
|
12
31
|
class << self
|
13
32
|
def new(*argz)
|
14
33
|
specs = argz
|
@@ -35,17 +54,50 @@ class Class2
|
|
35
54
|
end
|
36
55
|
end
|
37
56
|
|
38
|
-
def
|
57
|
+
def split_and_normalize_attributes(attributes)
|
58
|
+
nested = []
|
59
|
+
simple = []
|
60
|
+
|
39
61
|
attributes = [attributes] unless attributes.is_a?(Array)
|
62
|
+
attributes.compact.each do |attr|
|
63
|
+
# Just an attribute name, no type, so use default type String
|
64
|
+
if !attr.is_a?(Hash)
|
65
|
+
simple << { attr => nil }
|
66
|
+
next
|
67
|
+
end
|
68
|
+
|
69
|
+
attr.each do |k, v|
|
70
|
+
if v.is_a?(Hash) || v.is_a?(Array)
|
71
|
+
if v.empty?
|
72
|
+
# If it's empty it's not a nested spec, the attributes type is a Hash or Array
|
73
|
+
simple << { k => v.class }
|
74
|
+
else
|
75
|
+
nested << { k => v }
|
76
|
+
end
|
77
|
+
else
|
78
|
+
# Type can be a class name or an instance
|
79
|
+
# If it's an instance, use its type
|
80
|
+
v = v.class unless v.is_a?(Class)
|
81
|
+
simple << { k => v }
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
[ nested, simple ]
|
87
|
+
end
|
40
88
|
|
41
|
-
|
89
|
+
def make_class(namespace, name, attributes)
|
90
|
+
nested, simple = split_and_normalize_attributes(attributes)
|
42
91
|
nested.each do |object|
|
43
92
|
object.each { |klass, attrs| make_class(namespace, klass, attrs) }
|
44
93
|
end
|
45
94
|
|
95
|
+
make_method_name = lambda { |x| x.to_s.gsub(/[^\w]+/, "_") } # good enough
|
96
|
+
|
46
97
|
klass = Class.new do
|
47
98
|
def initialize(attributes = nil)
|
48
|
-
|
99
|
+
return unless attributes.is_a?(Hash)
|
100
|
+
assign_attributes(attributes)
|
49
101
|
end
|
50
102
|
|
51
103
|
class_eval <<-CODE
|
@@ -62,21 +114,54 @@ class Class2
|
|
62
114
|
|
63
115
|
def to_h
|
64
116
|
hash = {}
|
65
|
-
(#{simple + nested.map { |n| n.keys.first }}).each do |name|
|
117
|
+
(#{simple.map { |n| n.keys.first } + nested.map { |n| n.keys.first }}).each do |name|
|
66
118
|
hash[name] = public_send(name)
|
67
|
-
|
119
|
+
next unless hash[name].respond_to?(:to_h)
|
120
|
+
|
121
|
+
errors = [ ArgumentError, TypeError ]
|
122
|
+
# Seems needlessly complicated, why doesn't Hash() do some of this?
|
123
|
+
begin
|
124
|
+
hash[name] = hash[name].to_h
|
125
|
+
# to_h is dependent on its contents
|
126
|
+
rescue *errors
|
127
|
+
next unless hash[name].is_a?(Enumerable)
|
128
|
+
hash[name] = hash[name].map do |e|
|
129
|
+
begin
|
130
|
+
e.respond_to?(:to_h) ? e.to_h : e
|
131
|
+
rescue *errors
|
132
|
+
# Give up
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
68
136
|
end
|
69
137
|
|
70
138
|
hash
|
71
139
|
end
|
140
|
+
|
141
|
+
def __nested_attributes
|
142
|
+
#{nested.map { |n| n.keys.first.to_sym }}.freeze
|
143
|
+
end
|
144
|
+
|
145
|
+
private :__nested_attributes
|
72
146
|
CODE
|
73
147
|
|
74
|
-
simple.each
|
148
|
+
simple.each do |cfg|
|
149
|
+
method, type = cfg.first
|
150
|
+
method = make_method_name[method]
|
151
|
+
|
152
|
+
attr_reader method
|
153
|
+
|
154
|
+
class_eval <<-CODE
|
155
|
+
def #{method}=(v)
|
156
|
+
@#{method} = #{CONVERSIONS[type]["v"]}
|
157
|
+
end
|
158
|
+
CODE
|
159
|
+
end
|
75
160
|
|
76
161
|
nested.map { |n| n.keys.first }.each do |method, _|
|
162
|
+
method = make_method_name[method]
|
77
163
|
attr_writer method
|
78
164
|
|
79
|
-
method = method.to_s
|
80
165
|
retval = method == method.pluralize ? "[]" : "#{method.classify}.new"
|
81
166
|
class_eval <<-CODE
|
82
167
|
def #{method}
|
@@ -89,17 +174,20 @@ class Class2
|
|
89
174
|
|
90
175
|
def assign_attributes(attributes)
|
91
176
|
attributes.each do |key, value|
|
92
|
-
if
|
93
|
-
|
177
|
+
if __nested_attributes.include?(key.respond_to?(:to_sym) ? key.to_sym : key) &&
|
178
|
+
(value.is_a?(Hash) || value.is_a?(Array))
|
179
|
+
|
180
|
+
name = key.to_s.classify
|
94
181
|
|
95
182
|
# Only look in our namespace to prevent unwanted lookup
|
96
183
|
next unless self.class.parent.const_defined?(name)
|
97
|
-
klass = self.class.parent.const_get(name)
|
98
184
|
|
185
|
+
klass = self.class.parent.const_get(name)
|
99
186
|
value = value.is_a?(Hash) ? klass.new(value) : value.map { |v| klass.new(v) }
|
100
187
|
end
|
101
188
|
|
102
|
-
|
189
|
+
method = "#{key}="
|
190
|
+
public_send(method, value) if respond_to?(method)
|
103
191
|
end
|
104
192
|
end
|
105
193
|
end
|
data/lib/class2/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: class2
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Skye Shaw
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-08-
|
11
|
+
date: 2017-08-10 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -16,14 +16,14 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '3.
|
19
|
+
version: '3.2'
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: '3.
|
26
|
+
version: '3.2'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: bundler
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -75,6 +75,7 @@ extra_rdoc_files: []
|
|
75
75
|
files:
|
76
76
|
- ".gitignore"
|
77
77
|
- ".travis.yml"
|
78
|
+
- Changes
|
78
79
|
- Gemfile
|
79
80
|
- LICENSE.txt
|
80
81
|
- README.md
|
@@ -105,6 +106,6 @@ rubyforge_project:
|
|
105
106
|
rubygems_version: 2.6.11
|
106
107
|
signing_key:
|
107
108
|
specification_version: 4
|
108
|
-
summary: Easily create hierarchies of classes that support nested attributes,
|
109
|
-
and more.
|
109
|
+
summary: Easily create hierarchies of classes that support nested attributes, type
|
110
|
+
conversion, equality, and more.
|
110
111
|
test_files: []
|