traitorous 0.3.0 → 0.6.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +85 -0
- data/README.md +156 -69
- data/VERSION +1 -1
- data/lib/traitorous/convert.rb +106 -0
- data/lib/traitorous/converter.rb +30 -0
- data/lib/traitorous/standard_procs.rb +35 -0
- data/lib/traitorous.rb +37 -29
- metadata +6 -8
- data/lib/traitorous/converter/default_value_static.rb +0 -15
- data/lib/traitorous/converter/identity.rb +0 -35
- data/lib/traitorous/converter/method_keyed_uniform_hash.rb +0 -43
- data/lib/traitorous/converter/model.rb +0 -39
- data/lib/traitorous/converter/uniform_array.rb +0 -17
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ce24a9926d5a1a1473d4ecce829f5ae67753747e
|
4
|
+
data.tar.gz: 935ffb3bb8ace183357d11db17ed1b9a45a07cd8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e4d983bdef166b6f527e01ab327d8e1ecdade88fec74f372bb0d534a31833ddbdf1921a23457cfe2b9fb16136f1547fe32dd1b92cf623c0853b6cd82906d5705
|
7
|
+
data.tar.gz: 5df97df341451212529e99b632aa18267c4a74648aa4f074e6be9ee2184617b8a830c5d71b40b76151907ff18014e600a7aea30873aa8ba5c06e0839b2a73a38
|
data/.gitignore
CHANGED
@@ -6,5 +6,90 @@
|
|
6
6
|
/doc/
|
7
7
|
/pkg/
|
8
8
|
/spec/reports/
|
9
|
+
### Ruby template
|
10
|
+
*.gem
|
11
|
+
*.rbc
|
12
|
+
/.config
|
13
|
+
/coverage/
|
14
|
+
/InstalledFiles
|
15
|
+
/pkg/
|
16
|
+
/spec/reports/
|
17
|
+
/spec/examples.txt
|
18
|
+
/test/tmp/
|
19
|
+
/test/version_tmp/
|
20
|
+
/tmp/
|
21
|
+
|
22
|
+
## Specific to RubyMotion:
|
23
|
+
.dat*
|
24
|
+
.repl_history
|
25
|
+
build/
|
26
|
+
|
27
|
+
## Documentation cache and generated files:
|
28
|
+
/.yardoc/
|
29
|
+
/_yardoc/
|
30
|
+
/doc/
|
31
|
+
/rdoc/
|
32
|
+
|
33
|
+
## Environment normalisation:
|
34
|
+
/.bundle/
|
35
|
+
/vendor/bundle
|
36
|
+
/lib/bundler/man/
|
37
|
+
|
38
|
+
# for a library or gem, you might want to ignore these files since the code is
|
39
|
+
# intended to run in multiple environments; otherwise, check them in:
|
40
|
+
# Gemfile.lock
|
41
|
+
# .ruby-version
|
42
|
+
# .ruby-gemset
|
43
|
+
|
44
|
+
# unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
|
45
|
+
.rvmrc
|
46
|
+
### JetBrains template
|
47
|
+
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio
|
48
|
+
|
49
|
+
*.iml
|
50
|
+
|
51
|
+
## Directory-based project format:
|
52
|
+
.idea/
|
53
|
+
# if you remove the above rule, at least ignore the following:
|
54
|
+
|
55
|
+
# User-specific stuff:
|
56
|
+
# .idea/workspace.xml
|
57
|
+
# .idea/tasks.xml
|
58
|
+
# .idea/dictionaries
|
59
|
+
|
60
|
+
# Sensitive or high-churn files:
|
61
|
+
# .idea/dataSources.ids
|
62
|
+
# .idea/dataSources.xml
|
63
|
+
# .idea/sqlDataSources.xml
|
64
|
+
# .idea/dynamic.xml
|
65
|
+
# .idea/uiDesigner.xml
|
66
|
+
|
67
|
+
# Gradle:
|
68
|
+
# .idea/gradle.xml
|
69
|
+
# .idea/libraries
|
70
|
+
|
71
|
+
# Mongo Explorer plugin:
|
72
|
+
# .idea/mongoSettings.xml
|
73
|
+
|
74
|
+
## File-based project format:
|
75
|
+
*.ipr
|
76
|
+
*.iws
|
77
|
+
|
78
|
+
## Plugin-specific files:
|
79
|
+
|
80
|
+
# IntelliJ
|
81
|
+
/out/
|
82
|
+
|
83
|
+
# mpeltonen/sbt-idea plugin
|
84
|
+
.idea_modules/
|
85
|
+
|
86
|
+
# JIRA plugin
|
87
|
+
atlassian-ide-plugin.xml
|
88
|
+
|
89
|
+
# Crashlytics plugin (for Android Studio and IntelliJ)
|
90
|
+
com_crashlytics_export_strings.xml
|
91
|
+
crashlytics.properties
|
92
|
+
crashlytics-build.properties
|
93
|
+
|
9
94
|
/tmp/
|
10
95
|
.env
|
data/README.md
CHANGED
@@ -39,51 +39,52 @@ Or install it yourself as:
|
|
39
39
|
|
40
40
|
```ruby
|
41
41
|
|
42
|
-
# see spec/
|
43
|
-
require 'traitorous'
|
44
|
-
|
45
|
-
class Ruin
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
end
|
51
|
-
|
52
|
-
r = Ruin.new(name: 'Skull Mountain', danger: 'The Devil of')
|
53
|
-
#
|
54
|
-
puts r.name
|
55
|
-
# Skull Mountain
|
56
|
-
|
57
|
-
puts r.danger
|
58
|
-
# The Devil of
|
59
|
-
|
60
|
-
puts r.export
|
61
|
-
# {
|
62
|
-
|
63
|
-
puts Ruin.new(r.export) == r
|
64
|
-
# true
|
65
|
-
|
66
|
-
class Area
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
end
|
73
|
-
|
74
|
-
area = Area.new(
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
42
|
+
# see spec/
|
43
|
+
require 'traitorous'
|
44
|
+
|
45
|
+
class Ruin
|
46
|
+
include Traitorous
|
47
|
+
include Traitorous::Equality
|
48
|
+
trait :name
|
49
|
+
trait :danger
|
50
|
+
end
|
51
|
+
|
52
|
+
r = Ruin.new(name: 'Skull Mountain', danger: 'The Devil of')
|
53
|
+
#
|
54
|
+
puts r.name
|
55
|
+
# Skull Mountain
|
56
|
+
|
57
|
+
puts r.danger
|
58
|
+
# The Devil of
|
59
|
+
|
60
|
+
puts r.export
|
61
|
+
# {"name"=>"Skull Mountain", "danger"=>"The Devil of"}
|
62
|
+
|
63
|
+
puts Ruin.new(r.export) == r
|
64
|
+
# true
|
65
|
+
|
66
|
+
class Area
|
67
|
+
include Traitorous
|
68
|
+
include Traitorous::Equality
|
69
|
+
trait :name
|
70
|
+
trait :size, Converter::DefaultValueStatic.new('sub-continent')
|
71
|
+
trait :ruins, Converter::UniformArray.new(Ruin)
|
72
|
+
end
|
73
|
+
|
74
|
+
area = Area.new(
|
75
|
+
name: 'Western Marches',
|
76
|
+
ruins: [{name: 'Skull Mountain', danger: 'The Devil of'},
|
77
|
+
{name: 'Dire Swamp', danger: 'The Devil of'}
|
78
|
+
]
|
79
|
+
)
|
80
|
+
puts area.size
|
81
|
+
# 'sub-continent'
|
82
|
+
puts area.ruins.length
|
83
|
+
# 2
|
84
|
+
puts area.export
|
85
|
+
# {:name=>"Western Slope", :size=> "sub-continent", :ruins=>[{:name=>"Dire Swamp", :danger=>"The Creature of"}, {:name=>"Skull Mountain", :danger=>"The Devil of"}]}
|
86
|
+
puts Area.new(area.export) == area
|
87
|
+
# true
|
87
88
|
```
|
88
89
|
|
89
90
|
## Converters
|
@@ -95,30 +96,114 @@ in a simple form ready to save.
|
|
95
96
|
This system should be flexible enough to account for an large variety of data
|
96
97
|
structures that can be read in and out of storage easily and in 1 tree.
|
97
98
|
|
98
|
-
|
99
|
-
|
100
|
-
|
99
|
+
The 5 that are in the system should cover a lot of ground, anything trickier
|
100
|
+
should just be implemented as it's own Converter, ensuring that it responds to
|
101
|
+
`#do_import(data)` and `#do_export(data)`.
|
101
102
|
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
103
|
+
Most of the converters wil have a pair of optional arguments: import_method
|
104
|
+
(defaults to :new) and export_method (defaults to :export). Export is simple,
|
105
|
+
it's the method called on the object, objects created using Traitorous
|
106
|
+
automatically respond_to export. set export_method to :to_s in order to convert
|
107
|
+
your Pathname object into a string. Set import_method to :parse, to create your
|
108
|
+
object with URI.parse. This should provide a tremendous amount of versatility.
|
106
109
|
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
110
|
+
```ruby
|
111
|
+
# Example of using the defaults for IMport_method and export_method
|
112
|
+
# import_method defaults to :new
|
113
|
+
# export_method defaults to :export
|
114
|
+
batman_data = {name: 'Batman', secret_identity: 'Bruce Wayne'}
|
115
|
+
converter = Model.new(Superhero)
|
116
|
+
superhero = converter.do_import(batman_data)
|
117
|
+
# does this
|
118
|
+
Superhero.send(import_method, data)
|
119
|
+
# and is the same result as
|
120
|
+
Superhero.new(batman_data)
|
121
|
+
# exports by
|
122
|
+
converter.do_export(superhero)
|
123
|
+
# does this
|
124
|
+
superhero.send(export_method)
|
125
|
+
# and is the same result as
|
126
|
+
superhero.export
|
127
|
+
|
128
|
+
# Example of using a custom import_method and export_method
|
129
|
+
batman_data_uri = '/batman/current'
|
130
|
+
converter = Model.new(Superhero, import_method: :parse, export_method: :to_s)
|
131
|
+
superhero = converter.do_import(batman_data)
|
132
|
+
# does this
|
133
|
+
uri = URI.send(import_method, batman_data)
|
134
|
+
# and is the same result as
|
135
|
+
URI.parse(batman_data)
|
136
|
+
# exports by
|
137
|
+
converter.do_export(uri)
|
138
|
+
# does this
|
139
|
+
uri.send(export_method)
|
140
|
+
# and is the same result as
|
141
|
+
uri.to_s
|
142
|
+
```
|
111
143
|
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
144
|
+
##### export
|
145
|
+
My current design kind of places a lot of the responsibilities for the export
|
146
|
+
sequence of the objects themselves. And I'm both satisied with this design and
|
147
|
+
not at all sure that it's the right design. The importers gets the combination
|
148
|
+
of the klass and the import_method which makes for a near perfect amount of
|
149
|
+
coverage, they cover everything from URI.parse to File.read to Animal.new to
|
150
|
+
YAML.load_file. But the exporters are expected to get with just the
|
151
|
+
export_method setting to be called on the object itself.
|
152
|
+
|
153
|
+
THe concepts of the converters could easily be isolated into a single type of
|
154
|
+
converter, and assigning both an import and export converter. But think the
|
155
|
+
concept has 3 points against it, and they take the day over the advantages.
|
156
|
+
1. Maintaining the import and export parts of the converter in a single object
|
157
|
+
provides a semantic and symbolic relationship between the import and export
|
158
|
+
functions for a single trait. This should be encouraged to maintain a focus
|
159
|
+
upon synchronicity between import and export that is central to Traitorous.
|
160
|
+
2. having to define both an import converter and an export converter makes the
|
161
|
+
trait api less attractive.
|
162
|
+
3. It is really easy to write a custom converter in which you can implement
|
163
|
+
whatever logic you need to meet your needs.
|
164
|
+
4. The domain knowledge on how to export belongs in the class or object of the
|
165
|
+
model.
|
166
|
+
|
167
|
+
#### Traitorous::Converter::Value
|
168
|
+
The Value converter leaves an existing value alone, but may insert a default
|
169
|
+
value if no value exists. The ability to maintain nil vs false values is
|
170
|
+
unimportant to me and thus unaddressed. I highly recommend a small custom class
|
171
|
+
instead. Or submit a PR.
|
172
|
+
|
173
|
+
#### MISSING - Traitorous::Converter::Proc
|
174
|
+
The Proc converter would take a block in the initialize `that thes a value gets applied to
|
116
175
|
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
176
|
+
|
177
|
+
#### Traitorous::Converter::Model
|
178
|
+
This converter calls an import_method on a klass and passes the data as
|
179
|
+
attributes. Villain.send(:new, data), URI.send(:parse, data). This is meant to
|
180
|
+
provide class instantiation, complex conversion behavior, and service object
|
181
|
+
handling. On export the export_method is called om the data. data.send(:export),
|
182
|
+
data.send(:to_s).
|
183
|
+
|
184
|
+
#### Traitorous::Converter::Array
|
185
|
+
This converter imports an array of data and then uses klass and import_method as
|
186
|
+
with the Converter::Model. On export, the each element has the export_method
|
187
|
+
called on it and returns the transformed array.
|
188
|
+
`data_arr.map{|e| e.send(export_method)}`
|
189
|
+
|
190
|
+
#### Traitorous::Converter::ArrayToHash
|
191
|
+
This converter takes a key_method in addition to klass, import_method, and export
|
192
|
+
methods. It takes an array of data, imports them as the Array converter does.
|
193
|
+
But the sends the key_method to the imported object for use in a key/value pair.
|
194
|
+
|
195
|
+
### MISSING - Traitorous::Converter::Hash
|
196
|
+
A converter that takes a hash to import, and export. The tricky thing is how
|
197
|
+
the keys of the incoming hash are treated, and how the keys for the exported
|
198
|
+
hash are created. One way would be to discard the keys of the incoming hash, and
|
199
|
+
treat the array of values as the ArrayToHash converter does. A second way would
|
200
|
+
be to maintain the incoming keys as the outgoing keys. A third way would be to
|
201
|
+
expect to pass both the key and the value around as a pair making the import
|
202
|
+
look like klass.send(import_method, key, value), and expecting (k,v) as output
|
203
|
+
for export.
|
204
|
+
|
205
|
+
I haven't come up with a use case that has compelled me to solve this problem
|
206
|
+
yet, my needs are provided for by the ArrayToHash converter.
|
122
207
|
|
123
208
|
### More Converters
|
124
209
|
|
@@ -128,6 +213,9 @@ usage. This especially would be important if you wanted to import a list of
|
|
128
213
|
object that represents different klasses that are given with a sub_type or
|
129
214
|
sub_class attributes that are part of the do_import data.
|
130
215
|
|
216
|
+
Maybe also set traits to accept both import and export settings? Not sure if
|
217
|
+
this is necessary yet.
|
218
|
+
|
131
219
|
## Roadmap
|
132
220
|
|
133
221
|
1. Add better documentation
|
@@ -136,10 +224,9 @@ sub_class attributes that are part of the do_import data.
|
|
136
224
|
a. DefaultValueDynamic that stores code to run upon input or output
|
137
225
|
b. VariableArray that uses a sub-type in the opts to location the correct
|
138
226
|
class to instantiate instead of always using a uniform class
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
227
|
+
4. Validations?
|
228
|
+
5. translations?
|
229
|
+
6. HashWithIndifferentAccess
|
143
230
|
## Development
|
144
231
|
|
145
232
|
I use Guard to automate testing. It won't affect anything execpt the disk space
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.6.1
|
@@ -0,0 +1,106 @@
|
|
1
|
+
module Traitorous
|
2
|
+
# Convert provides a selection of helpers that will build converters for
|
3
|
+
# common situations.
|
4
|
+
module Convert
|
5
|
+
class << self
|
6
|
+
# .skip provides the simplest converter. It does nothing to the value
|
7
|
+
# except pass it through unchanged on both import and export
|
8
|
+
# @return [Traitorous::Converter]
|
9
|
+
def skip
|
10
|
+
Converter.new(
|
11
|
+
StandardProcs.noop,
|
12
|
+
StandardProcs.noop
|
13
|
+
)
|
14
|
+
end
|
15
|
+
|
16
|
+
# .default(default_value = nil) creates a StandardProc.default importer
|
17
|
+
# and a StandardProcs.noop exporter
|
18
|
+
# TODO add :export_with for consistency
|
19
|
+
# @param default_value [Object] value to set in cases that data is falsey
|
20
|
+
# @return [Traitorous::Converter]
|
21
|
+
def default(default_value = nil)
|
22
|
+
Converter.new(
|
23
|
+
StandardProcs.default(default_value),
|
24
|
+
StandardProcs.noop
|
25
|
+
)
|
26
|
+
end
|
27
|
+
|
28
|
+
|
29
|
+
# .call_on creates a StandardProc.call_on proc for the importer, and a
|
30
|
+
# StandardProc.call_on_self proc for the exporter.
|
31
|
+
# @param method_name [Symbol, String] method to send data
|
32
|
+
# @param with: [Symbol, String] method to send klass with data as params
|
33
|
+
# @param export_with: [Symbol, String] method to send klass with data as params
|
34
|
+
# @return [Traitorous::Converter]
|
35
|
+
def call_on(klass, with: :new, export_with: :export)
|
36
|
+
Converter.new(
|
37
|
+
StandardProcs.call_on(klass, with_method: with),
|
38
|
+
StandardProcs.call_on_self(export_with)
|
39
|
+
)
|
40
|
+
end
|
41
|
+
|
42
|
+
# .call_on_self creates a StandardProc.call_on_self proc for the importer
|
43
|
+
# with the with_method param, and a StandardProc.call_on_self with the
|
44
|
+
# export_with param
|
45
|
+
# @param with_method [Symbol, String] method to send data on import
|
46
|
+
# @param export_with [Symbol, String] method to send data on export
|
47
|
+
# @return [Traitorous::Converter]
|
48
|
+
def call_on_self(with_method, export_with: :export)
|
49
|
+
Converter.new(
|
50
|
+
StandardProcs.call_on_self(with_method),
|
51
|
+
StandardProcs.call_on_self(export_with)
|
52
|
+
)
|
53
|
+
end
|
54
|
+
|
55
|
+
|
56
|
+
# Creates an importer that runs the converter importer or exporter for
|
57
|
+
# each element of an array, return the resulting array
|
58
|
+
# @param converter [Traitorous::Converter] the converter to use for both
|
59
|
+
# import and export.
|
60
|
+
# @return [Traitorous::Converter]
|
61
|
+
def array(converter)
|
62
|
+
Converter.new(
|
63
|
+
proc { |data| Array(data).map { |entry| converter.importer.call(entry) } },
|
64
|
+
proc { |data| Array(data).map { |entry| converter.exporter.call(entry) } }
|
65
|
+
)
|
66
|
+
end
|
67
|
+
|
68
|
+
# TODO better to extract the inject proc into it's own StandardProc?
|
69
|
+
# @param value_converter [Traitorous::Converter] the converter to use for both
|
70
|
+
# import and export.
|
71
|
+
# @return [Traitorous::Converter]
|
72
|
+
def hash(value_converter)
|
73
|
+
Converter.new(
|
74
|
+
proc { |data| data.inject({}) { |h, (k, v)| h[k] = value_converter.importer.call(v); h } },
|
75
|
+
proc { |data| data.inject({}) { |h, (k, v)| h[k] = value_converter.exporter.call(v); h } },
|
76
|
+
)
|
77
|
+
end
|
78
|
+
|
79
|
+
# TODO better expecting key, value pairs as the output of the converter?
|
80
|
+
# Expects an incoming array, uses the param value_converter for each
|
81
|
+
# element, then uses the key_converter on the resulting value and stores
|
82
|
+
# the key, value pair in the resulting hash.
|
83
|
+
# @note. The value_converter is used before the key_converter, and the
|
84
|
+
# key_converter receives the output of the value_converter as it's data
|
85
|
+
# param.
|
86
|
+
# @param key_converter [Traitorous::Converter] the converter to use for to
|
87
|
+
# generate the hash key for a given value.
|
88
|
+
# @param value_converter [Traitorous::Converter] the converter to use for
|
89
|
+
# converting the value.
|
90
|
+
# @return [Traitorous::Converter]
|
91
|
+
def array_to_hash(key_converter, value_converter)
|
92
|
+
Converter.new(
|
93
|
+
proc do |data_arr|
|
94
|
+
data_arr.inject({}) do |h, d|
|
95
|
+
v = value_converter.importer.call(d)
|
96
|
+
k = key_converter.importer.call(v)
|
97
|
+
h[k] = v
|
98
|
+
h
|
99
|
+
end
|
100
|
+
end,
|
101
|
+
proc { |data_hsh| data_hsh.values.map { |value| value_converter.exporter.call(value) } }
|
102
|
+
)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Traitorous
|
2
|
+
# The purpose of the converters are to facilitate the importation of simple
|
3
|
+
# JSON or YAML, CSV data, and import that data into an arbitrarily nested
|
4
|
+
# tree of objects. And then to take those object and be able to export
|
5
|
+
# that data in a simple form ready to save, that is functionally identical to
|
6
|
+
#
|
7
|
+
# opts = {some: 'deep', data: ['structures']}
|
8
|
+
# r = Mission.new(opts)
|
9
|
+
# Mission.new(r.export) == r
|
10
|
+
class Converter
|
11
|
+
# if no other converter is declared, use the identity converter which
|
12
|
+
# merely pass through the data passed to it.
|
13
|
+
# DEFAULT_PROC is a default pass through proc
|
14
|
+
|
15
|
+
attr_accessor :converters, :importer, :exporter
|
16
|
+
|
17
|
+
def initialize(importer, exporter = StandardProcs.noop)
|
18
|
+
@importer = importer
|
19
|
+
@exporter = exporter
|
20
|
+
end
|
21
|
+
|
22
|
+
def import(data)
|
23
|
+
importer.call(data)
|
24
|
+
end
|
25
|
+
|
26
|
+
def export(data)
|
27
|
+
exporter.call(data)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Traitorous
|
2
|
+
module StandardProcs
|
3
|
+
class << self
|
4
|
+
NOOP = proc { |data| data }
|
5
|
+
|
6
|
+
# a proc that simply passes through it's input as output
|
7
|
+
def noop
|
8
|
+
NOOP
|
9
|
+
end
|
10
|
+
|
11
|
+
# a proc that simply passes through it's input if the input is truthy,
|
12
|
+
# otherwise it returns it's default value
|
13
|
+
# @param default_value [Object] the default value to use with data is
|
14
|
+
# falsey
|
15
|
+
# @return [Object] returns data || default_value
|
16
|
+
def default(default_value)
|
17
|
+
proc { |data| data || default_value }
|
18
|
+
end
|
19
|
+
|
20
|
+
# This proc calls method_name on data.
|
21
|
+
# @param method_name [Symbol, String] method to send data
|
22
|
+
# @return [Object] result of calling method_name on data
|
23
|
+
def call_on_self(method_name)
|
24
|
+
proc { |data| data.send(method_name) }
|
25
|
+
end
|
26
|
+
|
27
|
+
# This proc calls klass with :with and passes data as params
|
28
|
+
# @param klass [Class, Module, Object] obj to call
|
29
|
+
# @param with_method: [Symbol, String] method to send klass with data as params
|
30
|
+
def call_on(klass, with_method: :new)
|
31
|
+
proc {|data| klass.send(with_method, data)}
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
data/lib/traitorous.rb
CHANGED
@@ -1,5 +1,14 @@
|
|
1
1
|
require 'active_support/core_ext/hash/indifferent_access'
|
2
2
|
|
3
|
+
# Traitorous is a model trait library whose primary goal is to be able to
|
4
|
+
# import from a hash and have it initialize an object graph. It also exports
|
5
|
+
# back to a similar hash.
|
6
|
+
# The exported hash will be almost identical to the import hash with the sole
|
7
|
+
# exception being that any trait values that are left out of the initial hash
|
8
|
+
# will end up being in the export hash with a nil value.
|
9
|
+
# Similar libraries include https://github.com/solnic/virtus,
|
10
|
+
# https://github.com/cgriego/active_attr, and https://github.com/jetrockets/attrio
|
11
|
+
|
3
12
|
module Traitorous
|
4
13
|
|
5
14
|
# Use the HashWithIndifferentAccess from the activesupport gem.
|
@@ -13,57 +22,56 @@ module Traitorous
|
|
13
22
|
# values to be defined, and 2 methods: :do_import and :do_export.
|
14
23
|
# do_import is meant to take the data, and use the initial data to combine to
|
15
24
|
# build objects or other data structures.
|
16
|
-
|
17
|
-
|
18
|
-
# JSON or YAML data and import that data into an arbitrarily nested tree
|
19
|
-
# of objects. And then to take those object and be able to export that data
|
20
|
-
# in a simple form ready to save.
|
21
|
-
# opts = {some: 'deep', data: ['structures']}
|
22
|
-
# r = Mission.new(opts)
|
23
|
-
# Mission.new(r.export) == r
|
24
|
-
require 'traitorous/converter/identity'
|
25
|
-
require 'traitorous/converter/default_value_static'
|
26
|
-
require 'traitorous/converter/model'
|
27
|
-
require 'traitorous/converter/uniform_array'
|
28
|
-
require 'traitorous/converter/method_keyed_uniform_hash'
|
29
|
-
# if no other converter is declared, use the identity converter which
|
30
|
-
# merely pass through the data passed to it.
|
31
|
-
DEFAULT_CONVERTER = Converter::Identity.new
|
32
|
-
end
|
25
|
+
require 'traitorous/standard_procs'
|
26
|
+
require 'traitorous/converter'
|
33
27
|
|
28
|
+
# The convert module provides simple front ends to the Converter object
|
29
|
+
# constructor
|
30
|
+
require 'traitorous/convert'
|
34
31
|
require 'traitorous/equality'
|
35
32
|
|
36
33
|
module ClassMethods
|
34
|
+
# @!attribute [rw] traits
|
35
|
+
# @return [HashWithIndifferntAccess]
|
36
|
+
# There's no reason you can't manually alter the .traits hash, but be
|
37
|
+
# aware of the potential consequences upon your data when doing so.
|
37
38
|
attr_accessor :traits
|
38
39
|
|
39
|
-
|
40
|
+
# @param attr_name [Symbol,String] name of the attribute, follows normal
|
41
|
+
# method name rules
|
42
|
+
# @param converter [Traitorous::Converter,#do_import,#do_export] the converter
|
43
|
+
# this trait will use during import and export operations. A converter
|
44
|
+
# must implement #do_import(data) and #do_export(data) but can otherwise
|
45
|
+
# be any object or module.
|
46
|
+
#
|
47
|
+
# defaults to Traitorous::Converter::DEFAULT
|
48
|
+
def trait(attr_name, converter = Convert.skip)
|
40
49
|
self.traits ||= HASH.new
|
41
|
-
|
42
|
-
# :converter is in use
|
43
|
-
self.traits[attr_name] = {converter: converter}
|
50
|
+
self.traits[attr_name.intern] = converter
|
44
51
|
attr_accessor attr_name
|
45
52
|
end
|
46
53
|
end
|
47
54
|
|
55
|
+
# :nodoc:
|
48
56
|
def self.included(base)
|
49
57
|
base.extend(ClassMethods)
|
50
58
|
end
|
51
59
|
|
52
|
-
|
60
|
+
# use the key/value pairs of
|
61
|
+
def initialize(data = {})
|
53
62
|
raise "No traits have been defined for #{self.class}" unless self.class.traits
|
54
|
-
@
|
55
|
-
traits.each_pair do |new_trait,
|
56
|
-
|
57
|
-
val = converter.do_import(@opts.fetch(new_trait, nil))
|
63
|
+
@data = HASH.new(data)
|
64
|
+
traits.each_pair do |new_trait, converter|
|
65
|
+
val = converter.import(@data.fetch(new_trait, nil))
|
58
66
|
self.send("#{new_trait}=".intern, val)
|
59
67
|
end
|
60
68
|
end
|
61
69
|
|
62
70
|
def export
|
63
|
-
exported =
|
64
|
-
traits.each_pair do |trait_name,
|
71
|
+
exported = {}
|
72
|
+
traits.each_pair do |trait_name, converter|
|
65
73
|
attr_value = self.public_send(trait_name.intern)
|
66
|
-
exported[trait_name] =
|
74
|
+
exported[trait_name] = converter.export(attr_value)
|
67
75
|
end
|
68
76
|
exported
|
69
77
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: traitorous
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.6.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- scott m parrish
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-
|
11
|
+
date: 2015-10-10 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -102,12 +102,10 @@ files:
|
|
102
102
|
- bin/console
|
103
103
|
- bin/setup
|
104
104
|
- lib/traitorous.rb
|
105
|
-
- lib/traitorous/
|
106
|
-
- lib/traitorous/converter
|
107
|
-
- lib/traitorous/converter/method_keyed_uniform_hash.rb
|
108
|
-
- lib/traitorous/converter/model.rb
|
109
|
-
- lib/traitorous/converter/uniform_array.rb
|
105
|
+
- lib/traitorous/convert.rb
|
106
|
+
- lib/traitorous/converter.rb
|
110
107
|
- lib/traitorous/equality.rb
|
108
|
+
- lib/traitorous/standard_procs.rb
|
111
109
|
- traitorius.gemspec
|
112
110
|
homepage: https://github.com/anithri/traitorous
|
113
111
|
licenses:
|
@@ -130,7 +128,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
130
128
|
version: '0'
|
131
129
|
requirements: []
|
132
130
|
rubyforge_project:
|
133
|
-
rubygems_version: 2.4.
|
131
|
+
rubygems_version: 2.4.8
|
134
132
|
signing_key:
|
135
133
|
specification_version: 4
|
136
134
|
summary: A simple attribute, import, export system for PORO,
|
@@ -1,35 +0,0 @@
|
|
1
|
-
module Traitorous
|
2
|
-
module Converter
|
3
|
-
# The Identity converter is used as a simple default which performs the
|
4
|
-
# jobs of a Converter without changing the inputs at all.
|
5
|
-
#
|
6
|
-
# This converter can be used when you have simple data (strings, dates,
|
7
|
-
# numbers, hashes and arrays of the same) that doesn't need to be inflated
|
8
|
-
# into an object and needs no changes to be safely stored as the same.
|
9
|
-
class Identity
|
10
|
-
# do_export is called in order to take an existing piece of data and
|
11
|
-
# prepare it to be written to a simpler data structure.
|
12
|
-
#
|
13
|
-
# The Identity converter exports the same data it is passed without
|
14
|
-
# changing them at all.
|
15
|
-
#
|
16
|
-
# @params data [Object] the data object passed in to be exported
|
17
|
-
# @return [Object] exports data to a form simple enough to save as JSON
|
18
|
-
def do_export(data)
|
19
|
-
data
|
20
|
-
end
|
21
|
-
|
22
|
-
# do_import is called in order to take some opts and to turn them into
|
23
|
-
# instantiated objects, arrays, or other types of transformation
|
24
|
-
#
|
25
|
-
# The Identity converter imports the same opts it is passed without
|
26
|
-
# changing them at all.
|
27
|
-
#
|
28
|
-
# @params opts [Object] the options to be imported
|
29
|
-
# @return [Object] opts
|
30
|
-
def do_import(opts)
|
31
|
-
opts
|
32
|
-
end
|
33
|
-
end
|
34
|
-
end
|
35
|
-
end
|
@@ -1,43 +0,0 @@
|
|
1
|
-
module Traitorous
|
2
|
-
module Converter
|
3
|
-
# MethodKeyedUniformedHash is meant to take an array of a simple data
|
4
|
-
# structures and convert each into a uniform class, and then will call a
|
5
|
-
# key_method on that class and use it as the key in the returned hash.
|
6
|
-
#
|
7
|
-
# Exported data will be converted into an array calling do_export on each
|
8
|
-
# element
|
9
|
-
class MethodKeyedUniformHash
|
10
|
-
attr_accessor :key_method, :uniform_klass
|
11
|
-
# @param key_method [Symbol] the method to call on the uniform_klass instance
|
12
|
-
# to generate the key in the returned hash
|
13
|
-
# @param uniform_klass [Class, #new] the class to instantiate with each
|
14
|
-
# element of the do_import array
|
15
|
-
def initialize(key_method, uniform_klass)
|
16
|
-
@key_method = key_method
|
17
|
-
@uniform_klass = uniform_klass
|
18
|
-
end
|
19
|
-
|
20
|
-
# The import instantiates each element of the array as an instance of
|
21
|
-
# the uniform_klass, the key is determined by calling key_method on the
|
22
|
-
# instance and then they are joined to the result hash as a key,
|
23
|
-
# instance pair.
|
24
|
-
# @param arr_data [Array] the array of data to instantiate
|
25
|
-
# @return [Hash] hash containing key, instance pairs
|
26
|
-
def do_import(arr_data)
|
27
|
-
out = {}
|
28
|
-
Array(arr_data).each do |elem_data|
|
29
|
-
obj = uniform_klass.new(elem_data)
|
30
|
-
out[obj.send(key_method)] = obj
|
31
|
-
end
|
32
|
-
out
|
33
|
-
end
|
34
|
-
|
35
|
-
# @param hsh_data [Hash<obj,#export>] keys are ignored
|
36
|
-
# @return Array each element of the values of hsh_data has #export called
|
37
|
-
# on it.
|
38
|
-
def do_export(hsh_data)
|
39
|
-
hsh_data.values.map{|instance| instance.export }
|
40
|
-
end
|
41
|
-
end
|
42
|
-
end
|
43
|
-
end
|
@@ -1,39 +0,0 @@
|
|
1
|
-
module Traitorous
|
2
|
-
module Converter
|
3
|
-
# The Model converter is used a a simple way to instantiate an object from
|
4
|
-
# the model_class using the imported opts, and which will call .export on
|
5
|
-
# the data.
|
6
|
-
#
|
7
|
-
# This converter can be used when you have a set of opts that you can pass
|
8
|
-
# to a classes .new command in order to instantiate an object.
|
9
|
-
class Model
|
10
|
-
# @param model_klass [Class,#new] class to instantiate when importing
|
11
|
-
def initialize(model_klass)
|
12
|
-
@model_klass = model_klass
|
13
|
-
end
|
14
|
-
|
15
|
-
# do_export is called in order to take an existing piece of data and
|
16
|
-
# prepare it to be written to a simpler data structure.
|
17
|
-
#
|
18
|
-
# The model converter exports the data by calling .export on it
|
19
|
-
#
|
20
|
-
# @params data [Object] the data object passed in to be exported
|
21
|
-
# @return [Object] result of data.export
|
22
|
-
def do_export(data)
|
23
|
-
data.export
|
24
|
-
end
|
25
|
-
|
26
|
-
# do_import is called in order to take some opts and to turn them into
|
27
|
-
# instantiated objects, arrays, or other types of transformation
|
28
|
-
#
|
29
|
-
# The model converter imports the opts by instantiating a model_class
|
30
|
-
# object with it.
|
31
|
-
#
|
32
|
-
# @params opts [Object] the options to be exported
|
33
|
-
# @return [Object] result of model_klass.new(opts)
|
34
|
-
def do_import(opts)
|
35
|
-
@model_klass.new(opts)
|
36
|
-
end
|
37
|
-
end
|
38
|
-
end
|
39
|
-
end
|
@@ -1,17 +0,0 @@
|
|
1
|
-
module Traitorous
|
2
|
-
module Converter
|
3
|
-
class UniformArray
|
4
|
-
def initialize(uniform_klass)
|
5
|
-
@uniform_klass ||= uniform_klass
|
6
|
-
end
|
7
|
-
def do_export(data_arr)
|
8
|
-
Array(data_arr).map(&:export)
|
9
|
-
end
|
10
|
-
|
11
|
-
def do_import(opts_arr)
|
12
|
-
Array(opts_arr).map{|d| @uniform_klass.new(d)}
|
13
|
-
end
|
14
|
-
|
15
|
-
end
|
16
|
-
end
|
17
|
-
end
|