traitorous 0.3.0 → 0.6.1
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 +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
|