morfo 0.3.0 → 0.4.0

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.
@@ -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