class2 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
[![Build Status](https://travis-ci.org/sshaw/class2.svg?branch=master)](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: []
|