morfo 0.3.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,34 @@
1
+ module Morfo
2
+ module Tools
3
+ class FlattenHashKeys
4
+ attr_reader :input_hash
5
+
6
+ def initialize(input_hash)
7
+ @input_hash = input_hash.dup.freeze
8
+ end
9
+
10
+ def flatten
11
+ input_hash.inject({}) do |result_hash, (key, value)|
12
+ inner_hash = false
13
+ if value.is_a?(Hash)
14
+ inner_hash = true
15
+ value.each do |inner_key, inner_value|
16
+ if inner_value.is_a?(Hash)
17
+ inner_hash = true
18
+ end
19
+ result_hash.merge!("#{key}.#{inner_key}".to_sym => inner_value)
20
+ end
21
+ else
22
+ result_hash.merge!(key.to_sym => value)
23
+ end
24
+
25
+ if inner_hash
26
+ FlattenHashKeys.new(result_hash).flatten
27
+ else
28
+ result_hash
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -1,3 +1,3 @@
1
1
  module Morfo
2
- VERSION = '0.3.0'
2
+ VERSION = "0.4.0"
3
3
  end
@@ -1,27 +1,28 @@
1
1
  # coding: utf-8
2
- lib = File.expand_path('../lib', __FILE__)
2
+ lib = File.expand_path("../lib", __FILE__)
3
3
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
- require 'morfo/version'
4
+ require "morfo/version"
5
5
 
6
6
  Gem::Specification.new do |spec|
7
- spec.name = 'morfo'
7
+ spec.name = "morfo"
8
8
  spec.version = Morfo::VERSION
9
- spec.authors = ['Leif Gensert']
10
- spec.email = ['leif@propertybase.com']
9
+ spec.authors = ["Leif Gensert"]
10
+ spec.email = ["leif@propertybase.com"]
11
11
  spec.description = %q{This gem provides a DSL for converting one hash into another}
12
12
  spec.summary = %q{Inspired by ActiveImporter, this gem generically converts an array of hashes}
13
- spec.homepage = ''
14
- spec.license = 'MIT'
13
+ spec.homepage = ""
14
+ spec.license = "MIT"
15
15
 
16
16
  spec.files = `git ls-files`.split($/)
17
17
  spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
18
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
- spec.require_paths = ['lib']
19
+ spec.require_paths = ["lib"]
20
20
 
21
- spec.add_dependency 'rake'
22
- spec.add_dependency 'json'
23
- spec.add_dependency 'rubysl' if RUBY_ENGINE == 'rbx'
21
+ spec.add_dependency "rake"
22
+ spec.add_dependency "json"
23
+ spec.add_dependency "activesupport", ">= 3.2"
24
+ spec.add_dependency "rubysl" if RUBY_ENGINE == "rbx"
24
25
 
25
- spec.add_development_dependency 'bundler', '~> 1.3'
26
- spec.add_development_dependency 'rspec', '>= 2.14', '< 4.0'
26
+ spec.add_development_dependency "bundler", "~> 1.3"
27
+ spec.add_development_dependency "rspec", ">= 2.14", "< 4.0"
27
28
  end
@@ -0,0 +1,114 @@
1
+ require "spec_helper"
2
+
3
+ describe Morfo::Builder do
4
+ describe "#build" do
5
+ subject { described_class.new(definitions) }
6
+
7
+ let(:definitions) do
8
+ [
9
+ {
10
+ field: :tv_show_title,
11
+ from: :title,
12
+ }
13
+ ]
14
+ end
15
+
16
+ let(:morfer) { subject.build }
17
+
18
+ it "has a morf method" do
19
+ expect(morfer).to respond_to(:morf)
20
+ end
21
+
22
+ it "has a morf_single method" do
23
+ expect(morfer).to respond_to(:morf_single)
24
+ end
25
+ end
26
+
27
+ context "on the fly morfers" do
28
+ subject { described_class.new(definitions).build }
29
+
30
+ it_behaves_like "a 1 to 1 morfer" do
31
+ let(:definitions) do
32
+ [
33
+ { field: :tv_show_title, from: :title }
34
+ ]
35
+ end
36
+ end
37
+
38
+ it_behaves_like "a 1 to many morfer" do
39
+ let(:definitions) do
40
+ [
41
+ { field: :title, from: :title },
42
+ { field: :also_title, from: :title },
43
+ ]
44
+ end
45
+ end
46
+
47
+ it_behaves_like "a static morfer" do
48
+ let(:definitions) do
49
+ [
50
+ { field: :new_title, calculated: "Static Title" },
51
+ ]
52
+ end
53
+ end
54
+
55
+ it_behaves_like "a calculating morfer" do
56
+ let(:definitions) do
57
+ [
58
+ { field: :title_with_channel, calculated: "%{title}, (%{channel})" },
59
+ ]
60
+ end
61
+ end
62
+
63
+ it_behaves_like "a 1 to 1 morfer with transformation" do
64
+ let(:definitions) do
65
+ [
66
+ { field: :title, from: :title, transformed: "%{value} and Zombies" }
67
+ ]
68
+ end
69
+ end
70
+
71
+ it_behaves_like "a morfer with nested source" do
72
+ subject(:valid_path) do
73
+ described_class.new(
74
+ [
75
+ { field: :rating, from: [:ratings, :imdb] }
76
+ ]
77
+ ).build
78
+ end
79
+
80
+ subject(:valid_path_with_transformation) do
81
+ described_class.new(
82
+ [
83
+ { field: :rating, from: [:ratings, :imdb], transformed: "Rating: %{value}" }
84
+ ]
85
+ ).build
86
+ end
87
+
88
+ subject(:valid_path_with_calculation) do
89
+ described_class.new(
90
+ [
91
+ { field: :ratings, calculated: "IMDB: %{ratings.imdb}, Trakt: %{ratings.trakt}, Rotten Tommatoes: %{ratings.rotten_tomatoes}" }
92
+ ]
93
+ ).build
94
+ end
95
+
96
+ subject(:invalid_path) do
97
+ described_class.new(
98
+ [
99
+ { field: :rating, from: [:very, :long, :path, :that, :might, :not, :exist] }
100
+ ]
101
+ ).build
102
+ end
103
+ end
104
+
105
+ it_behaves_like "a morfer with nested destination" do
106
+ let(:definitions) do
107
+ [
108
+ { field: [:tv_show, :title], from: :title },
109
+ { field: [:tv_show, :channel], from: :channel, transformed: "Channel: %{value}" },
110
+ ]
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,91 @@
1
+ require "spec_helper"
2
+
3
+ module Morfo
4
+ module Tools
5
+ describe FlattenHashKeys do
6
+ subject { described_class.new(input_hash) }
7
+
8
+ context "symbol keys" do
9
+ context "flat hash" do
10
+ let(:input_hash) { { simple: :hash } }
11
+ let(:expected_output) { { simple: :hash } }
12
+
13
+ it "returns a new hash" do
14
+ expect(subject.flatten).not_to be(input_hash)
15
+ end
16
+
17
+ it "returns the correct hash" do
18
+ expect(subject.flatten).to eq(expected_output)
19
+ end
20
+ end
21
+
22
+ context "single nested hash" do
23
+ let(:input_hash) { { a: { nested: :hash } } }
24
+ let(:expected_output) { { :"a.nested" => :hash } }
25
+
26
+ it "returns a new hash" do
27
+ expect(subject.flatten).not_to be(input_hash)
28
+ end
29
+
30
+ it "returns the correct hash" do
31
+ expect(subject.flatten).to eq(expected_output)
32
+ end
33
+ end
34
+
35
+ context "multiple nested hash" do
36
+ let(:input_hash) { { a: { deeper: { nested: :hash } } } }
37
+ let(:expected_output) { { :"a.deeper.nested" => :hash } }
38
+
39
+ it "returns a new hash" do
40
+ expect(subject.flatten).not_to be(input_hash)
41
+ end
42
+
43
+ it "returns the correct hash" do
44
+ expect(subject.flatten).to eq(expected_output)
45
+ end
46
+ end
47
+ end
48
+
49
+ context "string keys" do
50
+ context "flat hash" do
51
+ let(:input_hash) { { "simple" => :hash } }
52
+ let(:expected_output) { { simple: :hash } }
53
+
54
+ it "returns a new hash" do
55
+ expect(subject.flatten).not_to be(input_hash)
56
+ end
57
+
58
+ it "returns the correct hash" do
59
+ expect(subject.flatten).to eq(expected_output)
60
+ end
61
+ end
62
+
63
+ context "single nested hash" do
64
+ let(:input_hash) { { "a" => { "nested" => :hash } } }
65
+ let(:expected_output) { { :"a.nested" => :hash } }
66
+
67
+ it "returns a new hash" do
68
+ expect(subject.flatten).not_to be(input_hash)
69
+ end
70
+
71
+ it "returns the correct hash" do
72
+ expect(subject.flatten).to eq(expected_output)
73
+ end
74
+ end
75
+
76
+ context "multiple nested hash" do
77
+ let(:input_hash) { { "a" => { "deeper" => { "nested" => :hash } } } }
78
+ let(:expected_output) { { :"a.deeper.nested" => :hash } }
79
+
80
+ it "returns a new hash" do
81
+ expect(subject.flatten).not_to be(input_hash)
82
+ end
83
+
84
+ it "returns the correct hash" do
85
+ expect(subject.flatten).to eq(expected_output)
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
@@ -1,287 +1,108 @@
1
- require 'spec_helper'
1
+ require "spec_helper"
2
2
 
3
3
  describe Morfo::Base do
4
- let(:input) do
5
- [
6
- {
7
- title: 'The Walking Dead',
8
- channel: 'AMC',
9
- watchers: 1337,
10
- status: 'running',
11
- cast: ['Lincoln, Andrew', 'McBride, Melissa'],
12
- ratings: {
13
- imdb: 8.7,
14
- trakt: 89,
15
- rotten_tomatoes: 93,
16
- },
17
- },
18
- {
19
- title: 'Breaking Bad',
20
- channel: 'AMC',
21
- watchers: 72891,
22
- status: 'ended',
23
- cast: ['Cranston, Bryan', 'Gunn, Anna'],
24
- ratings: {
25
- imdb: 9.5,
26
- trakt: 95,
27
- rotten_tomatoes: 100,
28
- },
29
- }
30
- ]
31
- end
32
-
33
- let(:single_input) do
34
- input.first
35
- end
36
-
37
- context 'errors' do
4
+ it_behaves_like "an error throwing morfer" do
38
5
  subject(:no_from) do
39
6
  class NilMorfer < Morfo::Base
40
7
  field(:my_field)
41
8
  end
42
9
  NilMorfer
43
10
  end
44
-
45
- describe '#morf' do
46
- it 'raises error for nil field' do
47
- expect{no_from.morf([{my_field: :something}])}.to raise_error(ArgumentError)
48
- end
49
- end
50
-
51
- describe '#morf_single' do
52
- it 'raises error for nil field' do
53
- expect{no_from.morf_single({my_field: :something})}.to raise_error(ArgumentError)
54
- end
55
- end
56
11
  end
57
12
 
58
- context '1 to 1 conversion' do
13
+ it_behaves_like "a 1 to 1 morfer" do
59
14
  subject do
60
15
  class TitleMorfer < Morfo::Base
61
16
  field(:tv_show_title).from(:title)
62
17
  end
63
18
  TitleMorfer
64
19
  end
65
-
66
- describe '#morf' do
67
- it 'maps title correctly' do
68
- expected_output = input.map{|v| {tv_show_title: v[:title]} }
69
- expect(subject.morf(input)).to eq(expected_output)
70
- end
71
-
72
- it 'leaves out nil values in result' do
73
- expected_output = [{},{}]
74
- modified_input = input.map{|h| h.reject{|k, v| k == :title}}
75
- expect(subject.morf(modified_input)).to eq(expected_output)
76
- end
77
- end
78
-
79
- describe '#morf_single' do
80
- it 'maps title correctly' do
81
- expected_output = { tv_show_title: single_input[:title] }
82
- expect(subject.morf_single(single_input)).to eq(expected_output)
83
- end
84
-
85
- it 'leaves out nil values in result' do
86
- expected_output = {}
87
- modified_input = single_input.reject { |k, v| k == :title }
88
- expect(subject.morf_single(modified_input)).to eq(expected_output)
89
- end
90
- end
91
20
  end
92
21
 
93
- context '1 to 1 conversion with transformation' do
22
+ it_behaves_like "a 1 to 1 morfer with transformation" do
94
23
  subject do
95
- class NumCastMorfer < Morfo::Base
96
- field(:cast_num).from(:cast).transformed{|v| v.size}
97
- end
98
- NumCastMorfer
99
- end
100
-
101
- describe '#morf' do
102
- it 'calls transformation correctly' do
103
- expected_output = input.map{|v| {cast_num: v[:cast].size} }
104
- expect(subject.morf(input)).to eq(expected_output)
105
- end
106
- end
107
-
108
- describe '#morf_single' do
109
- it 'calls transformation correctly' do
110
- expected_output = { cast_num: single_input[:cast].size }
111
- expect(subject.morf_single(single_input)).to eq(expected_output)
24
+ class AndZombies < Morfo::Base
25
+ field(:title).from(:title).transformed{|v| "#{v} and Zombies"}
112
26
  end
27
+ AndZombies
113
28
  end
114
29
  end
115
30
 
116
- context '1 to many conversion' do
31
+ it_behaves_like "a 1 to many morfer" do
117
32
  subject do
118
- class MutliTitleMorfer < Morfo::Base
33
+ class MultiTitleMorfer < Morfo::Base
119
34
  field(:title).from(:title)
120
35
  field(:also_title).from(:title)
121
36
  end
122
- MutliTitleMorfer
123
- end
124
-
125
- describe '#morf' do
126
- it 'maps title to multiple fields' do
127
- expected_output = input.map{|v| {title: v[:title], also_title: v[:title]} }
128
- expect(subject.morf(input)).to eq(expected_output)
129
- end
37
+ MultiTitleMorfer
130
38
  end
39
+ end
131
40
 
132
- describe '#morf_single' do
133
- it 'maps title to multiple fields' do
134
- expected_output = {title: single_input[:title], also_title: single_input[:title]}
135
- expect(subject.morf_single(single_input)).to eq(expected_output)
41
+ it_behaves_like "a calculating morfer" do
42
+ subject do
43
+ class TitlePrefixMorfer < Morfo::Base
44
+ field(:title_with_channel).calculated{|r| "#{r[:title]}, (#{r[:channel]})"}
136
45
  end
46
+ TitlePrefixMorfer
137
47
  end
138
48
  end
139
49
 
140
- context 'nested conversion' do
141
- context 'nested source' do
142
- subject(:valid_path) do
143
- class ImdbRatingMorfer < Morfo::Base
144
- field(:rating).from(:ratings, :imdb)
145
- end
146
- ImdbRatingMorfer
147
- end
148
-
149
- subject(:valid_path_with_transformation) do
150
- class ImdbRatingMorferWithTransformation < Morfo::Base
151
- field(:rating).from(:ratings, :imdb).transformed {|v| "Rating: #{v}"}
152
- end
153
- ImdbRatingMorferWithTransformation
154
- end
155
-
156
- subject(:invalid_path) do
157
- class InvalidImdbRatingMorfer < Morfo::Base
158
- field(:rating).from(:very, :long, :path, :that, :might, :not, :exist)
159
- end
160
- InvalidImdbRatingMorfer
161
- end
162
-
163
- describe '#morf' do
164
- it 'maps nested attributes' do
165
- expected_output = input.map{|v| {rating: v[:ratings][:imdb]} }
166
- expect(valid_path.morf(input)).to eq(expected_output)
167
- end
168
-
169
- it 'maps nested attributes with transformation' do
170
- expected_output = input.map{|v| {rating: "Rating: #{v[:ratings][:imdb]}"} }
171
- expect(valid_path_with_transformation.morf(input)).to eq(expected_output)
172
- end
173
-
174
- it 'doesn\'t raise error for invalid path' do
175
- expected_output = [{},{}]
176
- expect(invalid_path.morf(input)).to eq(expected_output)
177
- end
178
- end
179
-
180
- describe '#morf_single' do
181
- it 'maps nested attributes' do
182
- expected_output = {rating: single_input[:ratings][:imdb]}
183
- expect(valid_path.morf_single(single_input)).to eq(expected_output)
184
- end
185
-
186
- it 'maps nested attributes with transformation' do
187
- expected_output = {rating: "Rating: #{single_input[:ratings][:imdb]}"}
188
- expect(valid_path_with_transformation.morf_single(single_input)).to eq(expected_output)
189
- end
190
-
191
- it 'doesn\'t raise error for invalid path' do
192
- expected_output = { }
193
- expect(invalid_path.morf_single(single_input)).to eq(expected_output)
194
- end
50
+ it_behaves_like "a static morfer" do
51
+ subject do
52
+ class StaticTitleMorfer < Morfo::Base
53
+ field(:new_title).calculated{ "Static Title" }
195
54
  end
55
+ StaticTitleMorfer
196
56
  end
57
+ end
197
58
 
198
- context 'nested destination' do
199
- subject do
200
- class WrapperMorfer < Morfo::Base
201
- field(:tv_show, :title).from(:title)
202
- field(:tv_show, :channel).from(:channel).transformed {|v| "Channel: #{v}"}
203
- end
204
- WrapperMorfer
205
- end
206
-
207
- describe '#morf' do
208
- it 'maps to nested destination' do
209
- expected_output = input.map{|v|
210
- {
211
- tv_show: {
212
- title: v[:title],
213
- channel: "Channel: #{v[:channel]}",
214
- }
215
- }
216
- }
217
- expect(subject.morf(input)).to eq(expected_output)
218
- end
219
- end
220
-
221
- describe '#morf_single' do
222
- it 'maps to nested destination' do
223
- expected_output = {
224
- tv_show: {
225
- title: single_input[:title],
226
- channel: "Channel: #{single_input[:channel]}",
227
- }
228
- }
229
- expect(subject.morf_single(single_input)).to eq(expected_output)
230
- end
59
+ it_behaves_like "a morfer with nested source" do
60
+ subject(:valid_path) do
61
+ class ImdbRatingMorfer < Morfo::Base
62
+ field(:rating).from(:ratings, :imdb)
231
63
  end
64
+ ImdbRatingMorfer
232
65
  end
233
- end
234
66
 
235
- context 'calculations' do
236
- subject do
237
- class TitlePrefixMorfer < Morfo::Base
238
- field(:title_with_channel).calculated{|r| "#{r[:title]}, (#{r[:channel]})"}
67
+ subject(:valid_path_with_transformation) do
68
+ class ImdbRatingMorferWithTransformation < Morfo::Base
69
+ field(:rating).from(:ratings, :imdb).transformed {|v| "Rating: #{v}"}
239
70
  end
240
- TitlePrefixMorfer
71
+ ImdbRatingMorferWithTransformation
241
72
  end
242
73
 
243
- describe '#morf' do
244
- it 'maps calculation correctly' do
245
- expected_output = input.map{|r|
246
- {
247
- title_with_channel: "#{r[:title]}, (#{r[:channel]})"
248
- }
249
- }
250
- expect(subject.morf(input)).to eq(expected_output)
74
+ subject(:valid_path_with_calculation) do
75
+ class ImdbRatingMorferWithCalculation < Morfo::Base
76
+ field(:ratings).calculated {|r| "IMDB: #{r[:ratings][:imdb]}, Trakt: #{r[:ratings][:trakt]}, Rotten Tommatoes: #{r[:ratings][:rotten_tomatoes]}" }
251
77
  end
78
+ ImdbRatingMorferWithCalculation
252
79
  end
253
80
 
254
- describe '#morf_single' do
255
- it 'maps calculation correctly' do
256
- expected_output = {
257
- title_with_channel: "#{single_input[:title]}, (#{single_input[:channel]})"
258
- }
259
-
260
- expect(subject.morf_single(single_input)).to eq(expected_output)
81
+ subject(:invalid_path) do
82
+ class InvalidImdbRatingMorfer < Morfo::Base
83
+ field(:rating).from(:very, :long, :path, :that, :might, :not, :exist)
261
84
  end
85
+ InvalidImdbRatingMorfer
262
86
  end
263
87
  end
264
88
 
265
- context 'static values' do
89
+ it_behaves_like "a morfer with nested destination" do
266
90
  subject do
267
- class StaticTitleMorfer < Morfo::Base
268
- field(:new_title).calculated{ 'Static Title' }
269
- end
270
- StaticTitleMorfer
271
- end
272
-
273
- describe '#morf' do
274
- it 'maps static value correctly' do
275
- expected_output = input.map{|r| {new_title: 'Static Title'} }
276
- expect(subject.morf(input)).to eq(expected_output)
91
+ class WrapperMorfer < Morfo::Base
92
+ field(:tv_show, :title).from(:title)
93
+ field(:tv_show, :channel).from(:channel).transformed {|v| "Channel: #{v}"}
277
94
  end
95
+ WrapperMorfer
278
96
  end
97
+ end
279
98
 
280
- describe '#morf_single' do
281
- it 'maps static value correctly' do
282
- expected_output = { new_title: 'Static Title' }
283
- expect(subject.morf_single(single_input)).to eq(expected_output)
99
+ it_behaves_like "a morfer with nested destination" do
100
+ subject do
101
+ class WrapperMorfer < Morfo::Base
102
+ field(:tv_show, :title).from(:title)
103
+ field(:tv_show, :channel).from(:channel).transformed {|v| "Channel: #{v}"}
284
104
  end
105
+ WrapperMorfer
285
106
  end
286
107
  end
287
108
  end