From 0805398d1f7b0a314390ddffbda3fb16712d4eab Mon Sep 17 00:00:00 2001 From: Leif Gensert Date: Wed, 22 Apr 2015 13:00:45 -0700 Subject: [PATCH 01/16] coding style --- CONTRIBUTING.md | 2 +- Gemfile | 22 ++++---- Guardfile | 4 +- README.md | 38 ++++++------- Rakefile | 4 +- benchmarks/data.rb | 20 +++---- benchmarks/run.rb | 14 ++--- lib/morfo.rb | 5 +- lib/morfo/version.rb | 2 +- morfo.gemspec | 26 ++++----- spec/lib/morfo_spec.rb | 118 ++++++++++++++++++++--------------------- spec/spec_helper.rb | 10 ++-- 12 files changed, 133 insertions(+), 132 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0c9a6f3..cd629dd 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,6 +2,6 @@ 1. Fork it 2. Create your feature branch (`git checkout -b my-new-feature`) -3. Commit your changes (`git commit -am 'Add some feature'`) +3. Commit your changes (`git commit -am "Add some feature"`) 4. Push to the branch (`git push origin my-new-feature`) 5. Create new Pull Request diff --git a/Gemfile b/Gemfile index b92655f..c07214c 100644 --- a/Gemfile +++ b/Gemfile @@ -1,19 +1,19 @@ -source 'https://rubygems.org' +source "https://rubygems.org" # Specify your gem's dependencies in morfo.gemspec gemspec group :test, :development do - gem 'coveralls', require: false - gem 'guard' - gem 'guard-rspec' - gem 'simplecov' - gem 'pry' - gem 'rubinius-coverage', platform: :rbx + gem "coveralls", require: false + gem "guard" + gem "guard-rspec" + gem "simplecov" + gem "pry" + gem "rubinius-coverage", platform: :rbx - gem 'rb-inotify', require: false - gem 'rb-fsevent', require: false - gem 'rb-fchange', require: false + gem "rb-inotify", require: false + gem "rb-fsevent", require: false + gem "rb-fchange", require: false end -gem 'json' +gem "json" diff --git a/Guardfile b/Guardfile index c96cbe6..76e7247 100644 --- a/Guardfile +++ b/Guardfile @@ -1,8 +1,8 @@ # A sample Guardfile # More info at https://github.com/guard/guard#readme -guard :rspec, cmd: 'bundle exec rspec', all_on_start: true do +guard :rspec, cmd: "bundle exec rspec", all_on_start: true do watch(%r{^spec/.+_spec\.rb$}) watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" } - watch('spec/spec_helper.rb') { 'spec' } + watch("spec/spec_helper.rb") { "spec" } end diff --git a/README.md b/README.md index 2a7afd3..52d1119 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ This gem is currently only tested on Ruby 2.0 (including 2.0 mode of JRuby and R Add this line to your application's Gemfile: - gem 'morfo' + gem "morfo" And then execute: @@ -39,13 +39,13 @@ The most basic form is copying the value from another field. Afterwards use the `morf` method to morf all hashes in one array to the end result: Title.morf([ - {title: 'The Walking Dead'} , - {title: 'Breaking Bad'}, + {title: "The Walking Dead"} , + {title: "Breaking Bad"}, ]) # [ - # {tv_show_title: 'The Walking Dead'}, - # {tv_show_title: 'Breaking Bad'}, + # {tv_show_title: "The Walking Dead"}, + # {tv_show_title: "Breaking Bad"}, # ] If you want to have access to nested values, just provide the path to that field comma separated. @@ -59,21 +59,21 @@ If you want to have access to nested values, just provide the path to that field Name.morf([ { name: { - first: 'Clark', - last: 'Kent', + first: "Clark", + last: "Kent", }, }, { name: { - first: 'Bruce', - last: 'Wayne', + first: "Bruce", + last: "Wayne", }, }, ]) # [ - # {first_name: 'Clark',last_name: 'Kent'}, - # {first_name: 'Bruce',last_name: 'Wayne'}, + # {first_name: "Clark", last_name: "Kent"}, + # {first_name: "Bruce", last_name: "Wayne"}, # ] ## Transformations @@ -85,13 +85,13 @@ It's also possible to transform the value in any way ruby lets you transform a v end AndZombies.morf([ - {title: 'Pride and Prejudice'}, - {title: 'Fifty Shades of Grey'}, + {title: "Pride and Prejudice"}, + {title: "Fifty Shades of Grey"}, ]) # [ - # {title: 'Pride and Prejudice and Zombies'}, - # {title: 'Fifty Shades of Grey and Zombies'}, + # {title: "Pride and Prejudice and Zombies"}, + # {title: "Fifty Shades of Grey and Zombies"}, # ] ## Calculations @@ -100,15 +100,15 @@ If the value of your field should be based on multiple fields of the input row, class NameConcatenator < Morfo::Base field(:name).calculated {|row| "#{row[:first_name]} #{row[:last_name]}"} - field(:status).calculated {'Best Friend'} + field(:status).calculated {"Best Friend"} end NameConcatenator.morf([ - {first_name: 'Robin', last_name: 'Hood'}, - {first_name: 'Sherlock', last_name: 'Holmes'}, + {first_name: "Robin", last_name: "Hood"}, + {first_name: "Sherlock", last_name: "Holmes"}, ]) # [ # {:name=>"Robin Hood", :status=>"Best Friend"}, - # {:name=>"Sherlock Holmes", :status=>'Best Friend'} + # {:name=>"Sherlock Holmes", :status=>"Best Friend"} # ] diff --git a/Rakefile b/Rakefile index a81dfd8..7a720b9 100644 --- a/Rakefile +++ b/Rakefile @@ -1,4 +1,4 @@ -require 'bundler/gem_tasks' -require 'rspec/core/rake_task' +require "bundler/gem_tasks" +require "rspec/core/rake_task" RSpec::Core::RakeTask.new(:spec) diff --git a/benchmarks/data.rb b/benchmarks/data.rb index d5308c5..463b2f5 100644 --- a/benchmarks/data.rb +++ b/benchmarks/data.rb @@ -7,16 +7,16 @@ def nested_wrapper def row { - first_name: 'Jazmyn', - last_name: 'Willms', - gender: 'female', - phone_number: '485-675-9228', - cell_phone: '1-172-435-9402 x4907', - street_name: 'Becker Inlet', - street_number: '15a', - city: 'Carolynchester', - zip: '38189', - country: 'USA', + first_name: "Jazmyn", + last_name: "Willms", + gender: "female", + phone_number: "485-675-9228", + cell_phone: "1-172-435-9402 x4907", + street_name: "Becker Inlet", + street_number: "15a", + city: "Carolynchester", + zip: "38189", + country: "USA", } end diff --git a/benchmarks/run.rb b/benchmarks/run.rb index ffa56f1..01bfe92 100644 --- a/benchmarks/run.rb +++ b/benchmarks/run.rb @@ -1,28 +1,28 @@ -require 'morfo' -require 'benchmark' -require './benchmarks/data' +require "morfo" +require "benchmark" +require "./benchmarks/data" iterations = 100 batch_size = 10000 definitions = [ { - label: 'Simple (strings)', + label: "Simple (strings)", row: BenchmarkData.row_string_keys, morf_class: BenchmarkData::SimpleMorferString }, { - label: 'Simple (symbols)', + label: "Simple (symbols)", row: BenchmarkData.row, morf_class: BenchmarkData::SimpleMorferSymbol }, { - label: 'Nested (strings)', + label: "Nested (strings)", row: BenchmarkData.row_nested_string_keys, morf_class: BenchmarkData::NestedMorferString }, { - label: 'Nested (symbols)', + label: "Nested (symbols)", row: BenchmarkData.row_nested, morf_class: BenchmarkData::NestedMorferSymbol }, diff --git a/lib/morfo.rb b/lib/morfo.rb index a78905d..b0e5f26 100644 --- a/lib/morfo.rb +++ b/lib/morfo.rb @@ -1,5 +1,6 @@ -require 'morfo/version' -require 'morfo/actions' +require "morfo/version" +require "morfo/actions" +require "morfo/deserializer" module Morfo class Base diff --git a/lib/morfo/version.rb b/lib/morfo/version.rb index 644d127..c835bed 100644 --- a/lib/morfo/version.rb +++ b/lib/morfo/version.rb @@ -1,3 +1,3 @@ module Morfo - VERSION = '0.3.0' + VERSION = "0.3.0" end diff --git a/morfo.gemspec b/morfo.gemspec index 5ce58a6..cc6e57b 100644 --- a/morfo.gemspec +++ b/morfo.gemspec @@ -1,27 +1,27 @@ # coding: utf-8 -lib = File.expand_path('../lib', __FILE__) +lib = File.expand_path("../lib", __FILE__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) -require 'morfo/version' +require "morfo/version" Gem::Specification.new do |spec| - spec.name = 'morfo' + spec.name = "morfo" spec.version = Morfo::VERSION - spec.authors = ['Leif Gensert'] - spec.email = ['leif@propertybase.com'] + spec.authors = ["Leif Gensert"] + spec.email = ["leif@propertybase.com"] spec.description = %q{This gem provides a DSL for converting one hash into another} spec.summary = %q{Inspired by ActiveImporter, this gem generically converts an array of hashes} - spec.homepage = '' - spec.license = 'MIT' + spec.homepage = "" + spec.license = "MIT" spec.files = `git ls-files`.split($/) spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) - spec.require_paths = ['lib'] + spec.require_paths = ["lib"] - spec.add_dependency 'rake' - spec.add_dependency 'json' - spec.add_dependency 'rubysl' if RUBY_ENGINE == 'rbx' + spec.add_dependency "rake" + spec.add_dependency "json" + spec.add_dependency "rubysl" if RUBY_ENGINE == "rbx" - spec.add_development_dependency 'bundler', '~> 1.3' - spec.add_development_dependency 'rspec', '>= 2.14', '< 4.0' + spec.add_development_dependency "bundler", "~> 1.3" + spec.add_development_dependency "rspec", ">= 2.14", "< 4.0" end diff --git a/spec/lib/morfo_spec.rb b/spec/lib/morfo_spec.rb index f8bcc87..f9bb8f6 100644 --- a/spec/lib/morfo_spec.rb +++ b/spec/lib/morfo_spec.rb @@ -1,14 +1,14 @@ -require 'spec_helper' +require "spec_helper" describe Morfo::Base do let(:input) do [ { - title: 'The Walking Dead', - channel: 'AMC', + title: "The Walking Dead", + channel: "AMC", watchers: 1337, - status: 'running', - cast: ['Lincoln, Andrew', 'McBride, Melissa'], + status: "running", + cast: ["Lincoln, Andrew", "McBride, Melissa"], ratings: { imdb: 8.7, trakt: 89, @@ -16,11 +16,11 @@ }, }, { - title: 'Breaking Bad', - channel: 'AMC', + title: "Breaking Bad", + channel: "AMC", watchers: 72891, - status: 'ended', - cast: ['Cranston, Bryan', 'Gunn, Anna'], + status: "ended", + cast: ["Cranston, Bryan", "Gunn, Anna"], ratings: { imdb: 9.5, trakt: 95, @@ -34,7 +34,7 @@ input.first end - context 'errors' do + context "errors" do subject(:no_from) do class NilMorfer < Morfo::Base field(:my_field) @@ -42,20 +42,20 @@ class NilMorfer < Morfo::Base NilMorfer end - describe '#morf' do - it 'raises error for nil field' do + describe "#morf" do + it "raises error for nil field" do expect{no_from.morf([{my_field: :something}])}.to raise_error(ArgumentError) end end - describe '#morf_single' do - it 'raises error for nil field' do + describe "#morf_single" do + it "raises error for nil field" do expect{no_from.morf_single({my_field: :something})}.to raise_error(ArgumentError) end end end - context '1 to 1 conversion' do + context "1 to 1 conversion" do subject do class TitleMorfer < Morfo::Base field(:tv_show_title).from(:title) @@ -63,26 +63,26 @@ class TitleMorfer < Morfo::Base TitleMorfer end - describe '#morf' do - it 'maps title correctly' do + describe "#morf" do + it "maps title correctly" do expected_output = input.map{|v| {tv_show_title: v[:title]} } expect(subject.morf(input)).to eq(expected_output) end - it 'leaves out nil values in result' do + it "leaves out nil values in result" do expected_output = [{},{}] modified_input = input.map{|h| h.reject{|k, v| k == :title}} expect(subject.morf(modified_input)).to eq(expected_output) end end - describe '#morf_single' do - it 'maps title correctly' do + describe "#morf_single" do + it "maps title correctly" do expected_output = { tv_show_title: single_input[:title] } expect(subject.morf_single(single_input)).to eq(expected_output) end - it 'leaves out nil values in result' do + it "leaves out nil values in result" do expected_output = {} modified_input = single_input.reject { |k, v| k == :title } expect(subject.morf_single(modified_input)).to eq(expected_output) @@ -90,7 +90,7 @@ class TitleMorfer < Morfo::Base end end - context '1 to 1 conversion with transformation' do + context "1 to 1 conversion with transformation" do subject do class NumCastMorfer < Morfo::Base field(:cast_num).from(:cast).transformed{|v| v.size} @@ -98,22 +98,22 @@ class NumCastMorfer < Morfo::Base NumCastMorfer end - describe '#morf' do - it 'calls transformation correctly' do + describe "#morf" do + it "calls transformation correctly" do expected_output = input.map{|v| {cast_num: v[:cast].size} } expect(subject.morf(input)).to eq(expected_output) end end - describe '#morf_single' do - it 'calls transformation correctly' do + describe "#morf_single" do + it "calls transformation correctly" do expected_output = { cast_num: single_input[:cast].size } expect(subject.morf_single(single_input)).to eq(expected_output) end end end - context '1 to many conversion' do + context "1 to many conversion" do subject do class MutliTitleMorfer < Morfo::Base field(:title).from(:title) @@ -122,23 +122,23 @@ class MutliTitleMorfer < Morfo::Base MutliTitleMorfer end - describe '#morf' do - it 'maps title to multiple fields' do + describe "#morf" do + it "maps title to multiple fields" do expected_output = input.map{|v| {title: v[:title], also_title: v[:title]} } expect(subject.morf(input)).to eq(expected_output) end end - describe '#morf_single' do - it 'maps title to multiple fields' do + describe "#morf_single" do + it "maps title to multiple fields" do expected_output = {title: single_input[:title], also_title: single_input[:title]} expect(subject.morf_single(single_input)).to eq(expected_output) end end end - context 'nested conversion' do - context 'nested source' do + context "nested conversion" do + context "nested source" do subject(:valid_path) do class ImdbRatingMorfer < Morfo::Base field(:rating).from(:ratings, :imdb) @@ -160,42 +160,42 @@ class InvalidImdbRatingMorfer < Morfo::Base InvalidImdbRatingMorfer end - describe '#morf' do - it 'maps nested attributes' do + describe "#morf" do + it "maps nested attributes" do expected_output = input.map{|v| {rating: v[:ratings][:imdb]} } expect(valid_path.morf(input)).to eq(expected_output) end - it 'maps nested attributes with transformation' do + it "maps nested attributes with transformation" do expected_output = input.map{|v| {rating: "Rating: #{v[:ratings][:imdb]}"} } expect(valid_path_with_transformation.morf(input)).to eq(expected_output) end - it 'doesn\'t raise error for invalid path' do + it "doesn't raise error for invalid path" do expected_output = [{},{}] expect(invalid_path.morf(input)).to eq(expected_output) end end - describe '#morf_single' do - it 'maps nested attributes' do + describe "#morf_single" do + it "maps nested attributes" do expected_output = {rating: single_input[:ratings][:imdb]} expect(valid_path.morf_single(single_input)).to eq(expected_output) end - it 'maps nested attributes with transformation' do + it "maps nested attributes with transformation" do expected_output = {rating: "Rating: #{single_input[:ratings][:imdb]}"} expect(valid_path_with_transformation.morf_single(single_input)).to eq(expected_output) end - it 'doesn\'t raise error for invalid path' do + it "doesn't raise error for invalid path" do expected_output = { } expect(invalid_path.morf_single(single_input)).to eq(expected_output) end end end - context 'nested destination' do + context "nested destination" do subject do class WrapperMorfer < Morfo::Base field(:tv_show, :title).from(:title) @@ -204,8 +204,8 @@ class WrapperMorfer < Morfo::Base WrapperMorfer end - describe '#morf' do - it 'maps to nested destination' do + describe "#morf" do + it "maps to nested destination" do expected_output = input.map{|v| { tv_show: { @@ -218,8 +218,8 @@ class WrapperMorfer < Morfo::Base end end - describe '#morf_single' do - it 'maps to nested destination' do + describe "#morf_single" do + it "maps to nested destination" do expected_output = { tv_show: { title: single_input[:title], @@ -232,7 +232,7 @@ class WrapperMorfer < Morfo::Base end end - context 'calculations' do + context "calculations" do subject do class TitlePrefixMorfer < Morfo::Base field(:title_with_channel).calculated{|r| "#{r[:title]}, (#{r[:channel]})"} @@ -240,8 +240,8 @@ class TitlePrefixMorfer < Morfo::Base TitlePrefixMorfer end - describe '#morf' do - it 'maps calculation correctly' do + describe "#morf" do + it "maps calculation correctly" do expected_output = input.map{|r| { title_with_channel: "#{r[:title]}, (#{r[:channel]})" @@ -251,8 +251,8 @@ class TitlePrefixMorfer < Morfo::Base end end - describe '#morf_single' do - it 'maps calculation correctly' do + describe "#morf_single" do + it "maps calculation correctly" do expected_output = { title_with_channel: "#{single_input[:title]}, (#{single_input[:channel]})" } @@ -262,24 +262,24 @@ class TitlePrefixMorfer < Morfo::Base end end - context 'static values' do + context "static values" do subject do class StaticTitleMorfer < Morfo::Base - field(:new_title).calculated{ 'Static Title' } + field(:new_title).calculated{ "Static Title" } end StaticTitleMorfer end - describe '#morf' do - it 'maps static value correctly' do - expected_output = input.map{|r| {new_title: 'Static Title'} } + describe "#morf" do + it "maps static value correctly" do + expected_output = input.map{|r| {new_title: "Static Title"} } expect(subject.morf(input)).to eq(expected_output) end end - describe '#morf_single' do - it 'maps static value correctly' do - expected_output = { new_title: 'Static Title' } + describe "#morf_single" do + it "maps static value correctly" do + expected_output = { new_title: "Static Title" } expect(subject.morf_single(single_input)).to eq(expected_output) end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 39c452f..11c6f9b 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,10 +1,10 @@ -require 'simplecov' -require 'coveralls' +require "simplecov" +require "coveralls" SimpleCov.formatter = Coveralls::SimpleCov::Formatter SimpleCov.start do - add_filter 'spec' + add_filter "spec" end -require 'rspec' -require 'morfo' +require "rspec" +require "morfo" From d23b53f52bd8401bdff69d0173eb48c144c644ba Mon Sep 17 00:00:00 2001 From: Leif Gensert Date: Wed, 22 Apr 2015 16:08:33 -0700 Subject: [PATCH 02/16] centralize checking of correct output --- spec/lib/morfo_spec.rb | 270 +++++--------------------------- spec/spec_helper.rb | 2 + spec/support/shared_context.rb | 34 ++++ spec/support/shared_examples.rb | 227 +++++++++++++++++++++++++++ 4 files changed, 300 insertions(+), 233 deletions(-) create mode 100644 spec/support/shared_context.rb create mode 100644 spec/support/shared_examples.rb diff --git a/spec/lib/morfo_spec.rb b/spec/lib/morfo_spec.rb index f9bb8f6..618a9e6 100644 --- a/spec/lib/morfo_spec.rb +++ b/spec/lib/morfo_spec.rb @@ -1,287 +1,91 @@ require "spec_helper" describe Morfo::Base do - let(:input) do - [ - { - title: "The Walking Dead", - channel: "AMC", - watchers: 1337, - status: "running", - cast: ["Lincoln, Andrew", "McBride, Melissa"], - ratings: { - imdb: 8.7, - trakt: 89, - rotten_tomatoes: 93, - }, - }, - { - title: "Breaking Bad", - channel: "AMC", - watchers: 72891, - status: "ended", - cast: ["Cranston, Bryan", "Gunn, Anna"], - ratings: { - imdb: 9.5, - trakt: 95, - rotten_tomatoes: 100, - }, - } - ] - end - - let(:single_input) do - input.first - end - - context "errors" do + it_behaves_like "an error throwing morfer" do subject(:no_from) do class NilMorfer < Morfo::Base field(:my_field) end NilMorfer end - - describe "#morf" do - it "raises error for nil field" do - expect{no_from.morf([{my_field: :something}])}.to raise_error(ArgumentError) - end - end - - describe "#morf_single" do - it "raises error for nil field" do - expect{no_from.morf_single({my_field: :something})}.to raise_error(ArgumentError) - end - end end - context "1 to 1 conversion" do + it_behaves_like "a 1 to 1 morfer" do subject do class TitleMorfer < Morfo::Base field(:tv_show_title).from(:title) end TitleMorfer end - - describe "#morf" do - it "maps title correctly" do - expected_output = input.map{|v| {tv_show_title: v[:title]} } - expect(subject.morf(input)).to eq(expected_output) - end - - it "leaves out nil values in result" do - expected_output = [{},{}] - modified_input = input.map{|h| h.reject{|k, v| k == :title}} - expect(subject.morf(modified_input)).to eq(expected_output) - end - end - - describe "#morf_single" do - it "maps title correctly" do - expected_output = { tv_show_title: single_input[:title] } - expect(subject.morf_single(single_input)).to eq(expected_output) - end - - it "leaves out nil values in result" do - expected_output = {} - modified_input = single_input.reject { |k, v| k == :title } - expect(subject.morf_single(modified_input)).to eq(expected_output) - end - end end - context "1 to 1 conversion with transformation" do + it_behaves_like "a 1 to 1 morfer with transformation" do subject do - class NumCastMorfer < Morfo::Base - field(:cast_num).from(:cast).transformed{|v| v.size} - end - NumCastMorfer - end - - describe "#morf" do - it "calls transformation correctly" do - expected_output = input.map{|v| {cast_num: v[:cast].size} } - expect(subject.morf(input)).to eq(expected_output) - end - end - - describe "#morf_single" do - it "calls transformation correctly" do - expected_output = { cast_num: single_input[:cast].size } - expect(subject.morf_single(single_input)).to eq(expected_output) + class AndZombies < Morfo::Base + field(:title).from(:title).transformed{|v| "#{v} and Zombies"} end + AndZombies end end - context "1 to many conversion" do + it_behaves_like "a 1 to many morfer" do subject do - class MutliTitleMorfer < Morfo::Base + class MultiTitleMorfer < Morfo::Base field(:title).from(:title) field(:also_title).from(:title) end - MutliTitleMorfer - end - - describe "#morf" do - it "maps title to multiple fields" do - expected_output = input.map{|v| {title: v[:title], also_title: v[:title]} } - expect(subject.morf(input)).to eq(expected_output) - end - end - - describe "#morf_single" do - it "maps title to multiple fields" do - expected_output = {title: single_input[:title], also_title: single_input[:title]} - expect(subject.morf_single(single_input)).to eq(expected_output) - end + MultiTitleMorfer end end - context "nested conversion" do - context "nested source" do - subject(:valid_path) do - class ImdbRatingMorfer < Morfo::Base - field(:rating).from(:ratings, :imdb) - end - ImdbRatingMorfer - end - - subject(:valid_path_with_transformation) do - class ImdbRatingMorferWithTransformation < Morfo::Base - field(:rating).from(:ratings, :imdb).transformed {|v| "Rating: #{v}"} - end - ImdbRatingMorferWithTransformation - end - - subject(:invalid_path) do - class InvalidImdbRatingMorfer < Morfo::Base - field(:rating).from(:very, :long, :path, :that, :might, :not, :exist) - end - InvalidImdbRatingMorfer - end - - describe "#morf" do - it "maps nested attributes" do - expected_output = input.map{|v| {rating: v[:ratings][:imdb]} } - expect(valid_path.morf(input)).to eq(expected_output) - end - - it "maps nested attributes with transformation" do - expected_output = input.map{|v| {rating: "Rating: #{v[:ratings][:imdb]}"} } - expect(valid_path_with_transformation.morf(input)).to eq(expected_output) - end - - it "doesn't raise error for invalid path" do - expected_output = [{},{}] - expect(invalid_path.morf(input)).to eq(expected_output) - end - end - - describe "#morf_single" do - it "maps nested attributes" do - expected_output = {rating: single_input[:ratings][:imdb]} - expect(valid_path.morf_single(single_input)).to eq(expected_output) - end - - it "maps nested attributes with transformation" do - expected_output = {rating: "Rating: #{single_input[:ratings][:imdb]}"} - expect(valid_path_with_transformation.morf_single(single_input)).to eq(expected_output) - end - - it "doesn't raise error for invalid path" do - expected_output = { } - expect(invalid_path.morf_single(single_input)).to eq(expected_output) - end - end - end - - context "nested destination" do - subject do - class WrapperMorfer < Morfo::Base - field(:tv_show, :title).from(:title) - field(:tv_show, :channel).from(:channel).transformed {|v| "Channel: #{v}"} - end - WrapperMorfer - end - - describe "#morf" do - it "maps to nested destination" do - expected_output = input.map{|v| - { - tv_show: { - title: v[:title], - channel: "Channel: #{v[:channel]}", - } - } - } - expect(subject.morf(input)).to eq(expected_output) - end - end - - describe "#morf_single" do - it "maps to nested destination" do - expected_output = { - tv_show: { - title: single_input[:title], - channel: "Channel: #{single_input[:channel]}", - } - } - expect(subject.morf_single(single_input)).to eq(expected_output) - end - end - end - end - - context "calculations" do + it_behaves_like "a calculating morfer" do subject do class TitlePrefixMorfer < Morfo::Base field(:title_with_channel).calculated{|r| "#{r[:title]}, (#{r[:channel]})"} end TitlePrefixMorfer end + end - describe "#morf" do - it "maps calculation correctly" do - expected_output = input.map{|r| - { - title_with_channel: "#{r[:title]}, (#{r[:channel]})" - } - } - expect(subject.morf(input)).to eq(expected_output) + it_behaves_like "a static morfer" do + subject do + class StaticTitleMorfer < Morfo::Base + field(:new_title).calculated{ "Static Title" } end + StaticTitleMorfer end + end - describe "#morf_single" do - it "maps calculation correctly" do - expected_output = { - title_with_channel: "#{single_input[:title]}, (#{single_input[:channel]})" - } - - expect(subject.morf_single(single_input)).to eq(expected_output) + it_behaves_like "a morfer with nested source" do + subject(:valid_path) do + class ImdbRatingMorfer < Morfo::Base + field(:rating).from(:ratings, :imdb) end + ImdbRatingMorfer end - end - context "static values" do - subject do - class StaticTitleMorfer < Morfo::Base - field(:new_title).calculated{ "Static Title" } + subject(:valid_path_with_transformation) do + class ImdbRatingMorferWithTransformation < Morfo::Base + field(:rating).from(:ratings, :imdb).transformed {|v| "Rating: #{v}"} end - StaticTitleMorfer + ImdbRatingMorferWithTransformation end - describe "#morf" do - it "maps static value correctly" do - expected_output = input.map{|r| {new_title: "Static Title"} } - expect(subject.morf(input)).to eq(expected_output) + subject(:invalid_path) do + class InvalidImdbRatingMorfer < Morfo::Base + field(:rating).from(:very, :long, :path, :that, :might, :not, :exist) end + InvalidImdbRatingMorfer end + end - describe "#morf_single" do - it "maps static value correctly" do - expected_output = { new_title: "Static Title" } - expect(subject.morf_single(single_input)).to eq(expected_output) + it_behaves_like "a morfer with nested destination" do + subject do + class WrapperMorfer < Morfo::Base + field(:tv_show, :title).from(:title) + field(:tv_show, :channel).from(:channel).transformed {|v| "Channel: #{v}"} end + WrapperMorfer end end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 11c6f9b..1e4cbd3 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -8,3 +8,5 @@ require "rspec" require "morfo" + +Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f } diff --git a/spec/support/shared_context.rb b/spec/support/shared_context.rb new file mode 100644 index 0000000..a54436c --- /dev/null +++ b/spec/support/shared_context.rb @@ -0,0 +1,34 @@ +shared_context "tv shows" do + let(:input) do + [ + { + title: "The Walking Dead", + channel: "AMC", + watchers: 1337, + status: "running", + cast: ["Lincoln, Andrew", "McBride, Melissa"], + ratings: { + imdb: 8.7, + trakt: 89, + rotten_tomatoes: 93, + }, + }, + { + title: "Breaking Bad", + channel: "AMC", + watchers: 72891, + status: "ended", + cast: ["Cranston, Bryan", "Gunn, Anna"], + ratings: { + imdb: 9.5, + trakt: 95, + rotten_tomatoes: 100, + }, + } + ] + end + + let(:single_input) do + input.first + end +end diff --git a/spec/support/shared_examples.rb b/spec/support/shared_examples.rb new file mode 100644 index 0000000..68ea14d --- /dev/null +++ b/spec/support/shared_examples.rb @@ -0,0 +1,227 @@ +shared_examples "an error throwing morfer" do + include_context "tv shows" + + describe "#morf" do + it "raises error for nil field" do + expect{no_from.morf([{my_field: :something}])}.to raise_error(ArgumentError) + end + end + + describe "#morf_single" do + it "raises error for nil field" do + expect{no_from.morf_single({my_field: :something})}.to raise_error(ArgumentError) + end + end +end + +shared_examples "a 1 to 1 morfer" do + include_context "tv shows" + + describe "#morf" do + let(:expected_output) { input.map{|v| {tv_show_title: v[:title]} } } + + it "maps title correctly" do + expect(subject.morf(input)).to eq(expected_output) + end + + it "leaves out nil values in result" do + expected_output = [{},{}] + modified_input = input.map{|h| h.reject{|k, v| k == :title}} + expect(subject.morf(modified_input)).to eq(expected_output) + end + end + + describe "#morf_single" do + let(:expected_output) do + { tv_show_title: single_input[:title] } + end + + it "maps title correctly" do + expect(subject.morf_single(single_input)).to eq(expected_output) + end + + it "leaves out nil values in result" do + expected_output = {} + modified_input = single_input.reject { |k, v| k == :title } + expect(subject.morf_single(modified_input)).to eq(expected_output) + end + end +end + +shared_examples "a 1 to many morfer" do + include_context "tv shows" + + describe "#morf" do + let(:expected_output) { input.map{|v| {title: v[:title], also_title: v[:title]} } } + + it "maps title to multiple fields" do + expect(subject.morf(input)).to eq(expected_output) + end + end + + describe "#morf_single" do + let(:expected_output) { {title: single_input[:title], also_title: single_input[:title]} } + + it "maps title to multiple fields" do + expect(subject.morf_single(single_input)).to eq(expected_output) + end + end +end + +shared_examples "a 1 to 1 morfer with transformation" do + include_context "tv shows" + + describe "#morf" do + let(:expected_output) { input.map{|v| {title: "#{v[:title]} and Zombies"} } } + + it "calls transformation correctly" do + expect(subject.morf(input)).to eq(expected_output) + end + end + + describe "#morf_single" do + let(:expected_output) { { title: "#{single_input[:title]} and Zombies" } } + + it "calls transformation correctly" do + expect(subject.morf_single(single_input)).to eq(expected_output) + end + end +end + +shared_examples "a calculating morfer" do + include_context "tv shows" + + describe "#morf" do + let(:expected_output) do + input.map{|r| { title_with_channel: "#{r[:title]}, (#{r[:channel]})" } } + end + + it "maps calculation correctly" do + expect(subject.morf(input)).to eq(expected_output) + end + end + + describe "#morf_single" do + let(:expected_output) do + { + title_with_channel: "#{single_input[:title]}, (#{single_input[:channel]})" + } + end + + it "maps calculation correctly" do + expect(subject.morf_single(single_input)).to eq(expected_output) + end + end +end + +shared_examples "a static morfer" do + include_context "tv shows" + + describe "#morf" do + let(:expected_output) { input.map{|r| {new_title: "Static Title"} } } + + it "maps static value correctly" do + expect(subject.morf(input)).to eq(expected_output) + end + end + + describe "#morf_single" do + let(:expected_output) { { new_title: "Static Title" } } + + it "maps static value correctly" do + expect(subject.morf_single(single_input)).to eq(expected_output) + end + end +end + +shared_examples "a morfer with nested source" do + include_context "tv shows" + + describe "#morf" do + context "valid path" do + let(:expected_output) { input.map{|v| {rating: v[:ratings][:imdb]} } } + + it "maps nested attributes correctly" do + expect(valid_path.morf(input)).to eq(expected_output) + end + end + + context "valid path with transformation" do + let(:expected_output) { input.map{|v| {rating: "Rating: #{v[:ratings][:imdb]}"} } } + + it "maps and transforms nested attributes correctly" do + expect(valid_path_with_transformation.morf(input)).to eq(expected_output) + end + end + + context "invalid path" do + let(:expected_output) { [{},{}] } + + it "doesn't raise error for invalid path" do + expect(invalid_path.morf(input)).to eq(expected_output) + end + end + end + + describe "#morf_single" do + context "valid path" do + let(:expected_output) { {rating: single_input[:ratings][:imdb]} } + + it "maps nested attributes correctly" do + expect(valid_path.morf_single(single_input)).to eq(expected_output) + end + end + + context "valid path with transformation" do + let(:expected_output) { {rating: "Rating: #{single_input[:ratings][:imdb]}"} } + + it "maps nested attributes with transformation" do + expect(valid_path_with_transformation.morf_single(single_input)).to eq(expected_output) + end + end + + context "invalid path" do + let(:expected_output) { {} } + + it "doesn't raise error for invalid path" do + expect(invalid_path.morf_single(single_input)).to eq(expected_output) + end + end + end +end + +shared_examples "a morfer with nested destination" do + include_context "tv shows" + + describe "#morf" do + let(:expected_output) do + input.map do |v| + { + tv_show: { + title: v[:title], + channel: "Channel: #{v[:channel]}", + } + } + end + end + + it "maps to nested destination" do + expect(subject.morf(input)).to eq(expected_output) + end + end + + describe "#morf_single" do + let(:expected_output) do + { + tv_show: { + title: single_input[:title], + channel: "Channel: #{single_input[:channel]}", + } + } + end + + it "maps to nested destination" do + expect(subject.morf_single(single_input)).to eq(expected_output) + end + end +end From 1fee199822d39ad85c0eeaa17a0ba3160ce3a424 Mon Sep 17 00:00:00 2001 From: Leif Gensert Date: Wed, 22 Apr 2015 18:41:48 -0700 Subject: [PATCH 03/16] first implementation of builder --- lib/morfo.rb | 2 +- lib/morfo/builder.rb | 28 +++++++++++++++++ spec/lib/morfo/builder_spec.rb | 55 ++++++++++++++++++++++++++++++++++ 3 files changed, 84 insertions(+), 1 deletion(-) create mode 100644 lib/morfo/builder.rb create mode 100644 spec/lib/morfo/builder_spec.rb diff --git a/lib/morfo.rb b/lib/morfo.rb index b0e5f26..d5067ef 100644 --- a/lib/morfo.rb +++ b/lib/morfo.rb @@ -1,6 +1,6 @@ require "morfo/version" require "morfo/actions" -require "morfo/deserializer" +require "morfo/builder" module Morfo class Base diff --git a/lib/morfo/builder.rb b/lib/morfo/builder.rb new file mode 100644 index 0000000..fb145e1 --- /dev/null +++ b/lib/morfo/builder.rb @@ -0,0 +1,28 @@ +module Morfo + class Builder + def initialize(definitions) + @definitions = definitions + end + + def build + # WTF definitions is not accessable inside class + # so this javascript technique is necesseray + tmp_definitions = definitions + Class.new(Morfo::Base) do + tmp_definitions.each do |definition| + f = field(definition[:field]) + if definition[:from] + f = f.from(definition[:from]) + end + if definition[:static] + f = f.calculated { definition[:static] } + end + end + end + end + + private + + attr_reader :definitions + end +end diff --git a/spec/lib/morfo/builder_spec.rb b/spec/lib/morfo/builder_spec.rb new file mode 100644 index 0000000..da3ad13 --- /dev/null +++ b/spec/lib/morfo/builder_spec.rb @@ -0,0 +1,55 @@ +require "spec_helper" + +describe Morfo::Builder do + describe "#build" do + subject { described_class.new(definition) } + + let(:definition) do + [ + { + field: :tv_show_title, + from: :title, + } + ] + end + + let(:morfer) { subject.build } + + it "has a morf method" do + expect(morfer).to respond_to(:morf) + end + + it "has a morf_single method" do + expect(morfer).to respond_to(:morf_single) + end + end + + context "on the fly morfers" do + subject { described_class.new(definitions).build } + + it_behaves_like "a 1 to 1 morfer" do + let(:definitions) do + [ + { field: :tv_show_title, from: :title } + ] + end + end + + it_behaves_like "a 1 to many morfer" do + let(:definitions) do + [ + { field: :title, from: :title }, + { field: :also_title, from: :title }, + ] + end + end + + it_behaves_like "a static morfer" do + let(:definitions) do + [ + { field: :new_title, static: "Static Title" }, + ] + end + end + end +end From ed879ec1552767ecfc433821a54d1d8f49b2dbcf Mon Sep 17 00:00:00 2001 From: Leif Gensert Date: Thu, 23 Apr 2015 11:44:33 -0700 Subject: [PATCH 04/16] use activesupport for hash modifications --- lib/morfo.rb | 15 ++------------- morfo.gemspec | 1 + 2 files changed, 3 insertions(+), 13 deletions(-) diff --git a/lib/morfo.rb b/lib/morfo.rb index d5067ef..81d4530 100644 --- a/lib/morfo.rb +++ b/lib/morfo.rb @@ -1,3 +1,4 @@ +require "active_support/core_ext/hash" require "morfo/version" require "morfo/actions" require "morfo/builder" @@ -17,7 +18,7 @@ def self.morf input def self.morf_single input output = {} mapping_actions.each do |field_path, action| - deep_merge!(output, store_value(action.execute(input), field_path)) + output.deep_merge!(store_value(action.execute(input), field_path)) end output end @@ -38,17 +39,5 @@ def self.store_value value, to end end end - - def self.deep_merge! hash, other_hash, &block - other_hash.each_pair do |k,v| - tv = hash[k] - if tv.is_a?(Hash) && v.is_a?(Hash) - hash[k] = deep_merge!(tv, v, &block) - else - hash[k] = block && tv ? block.call(k, tv, v) : v - end - end - hash - end end end diff --git a/morfo.gemspec b/morfo.gemspec index cc6e57b..76fbf26 100644 --- a/morfo.gemspec +++ b/morfo.gemspec @@ -20,6 +20,7 @@ Gem::Specification.new do |spec| spec.add_dependency "rake" spec.add_dependency "json" + spec.add_dependency "activesupport", ">= 3.2" spec.add_dependency "rubysl" if RUBY_ENGINE == "rbx" spec.add_development_dependency "bundler", "~> 1.3" From b5c3863561594ab51feecd728302955ce5dc17e4 Mon Sep 17 00:00:00 2001 From: Leif Gensert Date: Thu, 23 Apr 2015 11:44:53 -0700 Subject: [PATCH 05/16] add possibilty of transformation --- lib/morfo/builder.rb | 3 +++ spec/lib/morfo/builder_spec.rb | 8 ++++++++ 2 files changed, 11 insertions(+) diff --git a/lib/morfo/builder.rb b/lib/morfo/builder.rb index fb145e1..ac212f3 100644 --- a/lib/morfo/builder.rb +++ b/lib/morfo/builder.rb @@ -17,6 +17,9 @@ def build if definition[:static] f = f.calculated { definition[:static] } end + if definition[:transformation] + f = f.transformed { |r| definition[:transformation] % {value: r} } + end end end end diff --git a/spec/lib/morfo/builder_spec.rb b/spec/lib/morfo/builder_spec.rb index da3ad13..d3281d6 100644 --- a/spec/lib/morfo/builder_spec.rb +++ b/spec/lib/morfo/builder_spec.rb @@ -51,5 +51,13 @@ ] end end + + it_behaves_like "a 1 to 1 morfer with transformation" do + let(:definitions) do + [ + {field: :title, from: :title, transformation: "%{value} and Zombies"} + ] + end + end end end From 008ea965a13db56be9130af70d889d0507b2e5f1 Mon Sep 17 00:00:00 2001 From: Leif Gensert Date: Thu, 23 Apr 2015 11:46:37 -0700 Subject: [PATCH 06/16] convert to symbolized keys --- lib/morfo/builder.rb | 2 +- spec/lib/morfo/builder_spec.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/morfo/builder.rb b/lib/morfo/builder.rb index ac212f3..0a2ba08 100644 --- a/lib/morfo/builder.rb +++ b/lib/morfo/builder.rb @@ -7,7 +7,7 @@ def initialize(definitions) def build # WTF definitions is not accessable inside class # so this javascript technique is necesseray - tmp_definitions = definitions + tmp_definitions = definitions.map { |h| h.symbolize_keys } Class.new(Morfo::Base) do tmp_definitions.each do |definition| f = field(definition[:field]) diff --git a/spec/lib/morfo/builder_spec.rb b/spec/lib/morfo/builder_spec.rb index d3281d6..fbee8a1 100644 --- a/spec/lib/morfo/builder_spec.rb +++ b/spec/lib/morfo/builder_spec.rb @@ -25,7 +25,7 @@ end context "on the fly morfers" do - subject { described_class.new(definitions).build } + subject { described_class.new(definitions).build } it_behaves_like "a 1 to 1 morfer" do let(:definitions) do From f2dd553ff943f202bed66727f7c55f1291719082 Mon Sep 17 00:00:00 2001 From: Leif Gensert Date: Thu, 23 Apr 2015 12:11:42 -0700 Subject: [PATCH 07/16] add spec for nested calculations --- spec/lib/morfo_spec.rb | 7 +++++++ spec/support/shared_examples.rb | 21 +++++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/spec/lib/morfo_spec.rb b/spec/lib/morfo_spec.rb index 618a9e6..61c95dc 100644 --- a/spec/lib/morfo_spec.rb +++ b/spec/lib/morfo_spec.rb @@ -71,6 +71,13 @@ class ImdbRatingMorferWithTransformation < Morfo::Base ImdbRatingMorferWithTransformation end + subject(:valid_path_with_calculation) do + class ImdbRatingMorferWithCalculation < Morfo::Base + field(:ratings).calculated {|r| "IMDB: #{r[:ratings][:imdb]}, Trakt: #{r[:ratings][:trakt]}, Rotten Tommatoes: #{r[:ratings][:rotten_tomatoes]}" } + end + ImdbRatingMorferWithCalculation + end + subject(:invalid_path) do class InvalidImdbRatingMorfer < Morfo::Base field(:rating).from(:very, :long, :path, :that, :might, :not, :exist) diff --git a/spec/support/shared_examples.rb b/spec/support/shared_examples.rb index 68ea14d..e95c4b7 100644 --- a/spec/support/shared_examples.rb +++ b/spec/support/shared_examples.rb @@ -154,6 +154,19 @@ end end + context "valid path with calculation" do + let(:expected_output) do + [ + { ratings: "IMDB: 8.7, Trakt: 89, Rotten Tommatoes: 93" }, + { ratings: "IMDB: 9.5, Trakt: 95, Rotten Tommatoes: 100" }, + ] + end + + it "maps nested attributes with transformation" do + expect(valid_path_with_calculation.morf(input)).to eq(expected_output) + end + end + context "invalid path" do let(:expected_output) { [{},{}] } @@ -180,6 +193,14 @@ end end + context "valid path with calculation" do + let(:expected_output) { {ratings: "IMDB: 8.7, Trakt: 89, Rotten Tommatoes: 93"} } + + it "maps nested attributes with transformation" do + expect(valid_path_with_calculation.morf_single(single_input)).to eq(expected_output) + end + end + context "invalid path" do let(:expected_output) { {} } From e7cf10edb430b3874a3616ce8fca1a926279165f Mon Sep 17 00:00:00 2001 From: Leif Gensert Date: Thu, 23 Apr 2015 12:58:46 -0700 Subject: [PATCH 08/16] add hash flattener --- lib/morfo/tools.rb | 30 ++++++++++++ spec/lib/morfo/tools_spec.rb | 91 ++++++++++++++++++++++++++++++++++++ 2 files changed, 121 insertions(+) create mode 100644 lib/morfo/tools.rb create mode 100644 spec/lib/morfo/tools_spec.rb diff --git a/lib/morfo/tools.rb b/lib/morfo/tools.rb new file mode 100644 index 0000000..2f2e2cf --- /dev/null +++ b/lib/morfo/tools.rb @@ -0,0 +1,30 @@ +module Morfo + module Tools + class FlattenHashKeys + attr_reader :input_hash + + def initialize(input_hash) + @input_hash = input_hash.dup.freeze + end + + def flatten + input_hash.inject({}) do |result_hash, (key, value)| + if value.is_a?(Hash) + value.each do |inner_key, inner_value| + if inner_value.is_a?(Hash) + FlattenHashKeys.new(value).flatten.each do |inner_inner_key, inner_inner_value| + result_hash.merge!("#{key}.#{inner_inner_key}".to_sym => inner_inner_value) + end + else + result_hash.merge!("#{key}.#{inner_key}".to_sym => inner_value) + end + end + else + result_hash.merge!(key.to_sym => value) + end + result_hash + end + end + end + end +end diff --git a/spec/lib/morfo/tools_spec.rb b/spec/lib/morfo/tools_spec.rb new file mode 100644 index 0000000..fde9457 --- /dev/null +++ b/spec/lib/morfo/tools_spec.rb @@ -0,0 +1,91 @@ +require "spec_helper" + +module Morfo + module Tools + describe FlattenHashKeys do + subject { described_class.new(input_hash) } + + context "symbol keys" do + context "flat hash" do + let(:input_hash) { { simple: :hash } } + let(:expected_output) { { simple: :hash } } + + it "returns a new hash" do + expect(subject.flatten).not_to be(input_hash) + end + + it "returns the correct hash" do + expect(subject.flatten).to eq(expected_output) + end + end + + context "single nested hash" do + let(:input_hash) { { a: { nested: :hash } } } + let(:expected_output) { { :"a.nested" => :hash } } + + it "returns a new hash" do + expect(subject.flatten).not_to be(input_hash) + end + + it "returns the correct hash" do + expect(subject.flatten).to eq(expected_output) + end + end + + context "multiple nested hash" do + let(:input_hash) { { a: { deeper: { nested: :hash } } } } + let(:expected_output) { { :"a.deeper.nested" => :hash } } + + it "returns a new hash" do + expect(subject.flatten).not_to be(input_hash) + end + + it "returns the correct hash" do + expect(subject.flatten).to eq(expected_output) + end + end + end + + context "string keys" do + context "flat hash" do + let(:input_hash) { { "simple" => :hash } } + let(:expected_output) { { simple: :hash } } + + it "returns a new hash" do + expect(subject.flatten).not_to be(input_hash) + end + + it "returns the correct hash" do + expect(subject.flatten).to eq(expected_output) + end + end + + context "single nested hash" do + let(:input_hash) { { "a" => { "nested" => :hash } } } + let(:expected_output) { { :"a.nested" => :hash } } + + it "returns a new hash" do + expect(subject.flatten).not_to be(input_hash) + end + + it "returns the correct hash" do + expect(subject.flatten).to eq(expected_output) + end + end + + context "multiple nested hash" do + let(:input_hash) { { "a" => { "deeper" => { "nested" => :hash } } } } + let(:expected_output) { { :"a.deeper.nested" => :hash } } + + it "returns a new hash" do + expect(subject.flatten).not_to be(input_hash) + end + + it "returns the correct hash" do + expect(subject.flatten).to eq(expected_output) + end + end + end + end + end +end From c383522733f05e3d743b15d4d66709385fa18540 Mon Sep 17 00:00:00 2001 From: Leif Gensert Date: Thu, 23 Apr 2015 13:00:35 -0700 Subject: [PATCH 09/16] add possibility to add calculations in nested hashes --- lib/morfo.rb | 1 + lib/morfo/builder.rb | 17 +++++++----- spec/lib/morfo/builder_spec.rb | 50 +++++++++++++++++++++++++++++++--- 3 files changed, 57 insertions(+), 11 deletions(-) diff --git a/lib/morfo.rb b/lib/morfo.rb index 81d4530..71396ad 100644 --- a/lib/morfo.rb +++ b/lib/morfo.rb @@ -1,5 +1,6 @@ require "active_support/core_ext/hash" require "morfo/version" +require "morfo/tools" require "morfo/actions" require "morfo/builder" diff --git a/lib/morfo/builder.rb b/lib/morfo/builder.rb index 0a2ba08..dac38af 100644 --- a/lib/morfo/builder.rb +++ b/lib/morfo/builder.rb @@ -5,20 +5,23 @@ def initialize(definitions) end def build - # WTF definitions is not accessable inside class + # WTF??? `definitions` is not accessible inside class # so this javascript technique is necesseray tmp_definitions = definitions.map { |h| h.symbolize_keys } Class.new(Morfo::Base) do tmp_definitions.each do |definition| - f = field(definition[:field]) + f = field(*definition[:field]) + if definition[:from] - f = f.from(definition[:from]) + f = f.from(*definition[:from]) end - if definition[:static] - f = f.calculated { definition[:static] } + + if definition[:calculated] + f = f.calculated { |r| definition[:calculated] % Morfo::Tools::FlattenHashKeys.new(r).flatten } end - if definition[:transformation] - f = f.transformed { |r| definition[:transformation] % {value: r} } + + if definition[:transformed] + f = f.transformed { |v| definition[:transformed] % {value: v} } end end end diff --git a/spec/lib/morfo/builder_spec.rb b/spec/lib/morfo/builder_spec.rb index fbee8a1..a40be56 100644 --- a/spec/lib/morfo/builder_spec.rb +++ b/spec/lib/morfo/builder_spec.rb @@ -2,9 +2,9 @@ describe Morfo::Builder do describe "#build" do - subject { described_class.new(definition) } + subject { described_class.new(definitions) } - let(:definition) do + let(:definitions) do [ { field: :tv_show_title, @@ -47,7 +47,15 @@ it_behaves_like "a static morfer" do let(:definitions) do [ - { field: :new_title, static: "Static Title" }, + { field: :new_title, calculated: "Static Title" }, + ] + end + end + + it_behaves_like "a calculating morfer" do + let(:definitions) do + [ + { field: :title_with_channel, calculated: "%{title}, (%{channel})" }, ] end end @@ -55,9 +63,43 @@ it_behaves_like "a 1 to 1 morfer with transformation" do let(:definitions) do [ - {field: :title, from: :title, transformation: "%{value} and Zombies"} + {field: :title, from: :title, transformed: "%{value} and Zombies"} ] end end + + it_behaves_like "a morfer with nested source" do + subject(:valid_path) do + described_class.new( + [ + { field: :rating, from: [:ratings, :imdb] } + ] + ).build + end + + subject(:valid_path_with_transformation) do + described_class.new( + [ + { field: :rating, from: [:ratings, :imdb], transformed: "Rating: %{value}" } + ] + ).build + end + + subject(:valid_path_with_calculation) do + described_class.new( + [ + { field: :ratings, calculated: "IMDB: %{ratings.imdb}, Trakt: %{ratings.trakt}, Rotten Tommatoes: %{ratings.rotten_tomatoes}" } + ] + ).build + end + + subject(:invalid_path) do + described_class.new( + [ + { field: :rating, from: [:very, :long, :path, :that, :might, :not, :exist] } + ] + ).build + end + end end end From 4b417688264c435e6c35cbcf40ccb24a581b8fb5 Mon Sep 17 00:00:00 2001 From: Leif Gensert Date: Thu, 23 Apr 2015 13:10:19 -0700 Subject: [PATCH 10/16] slightly improve readability of hash merging --- lib/morfo/tools.rb | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/lib/morfo/tools.rb b/lib/morfo/tools.rb index 2f2e2cf..1ddbb54 100644 --- a/lib/morfo/tools.rb +++ b/lib/morfo/tools.rb @@ -9,20 +9,24 @@ def initialize(input_hash) def flatten input_hash.inject({}) do |result_hash, (key, value)| + inner_hash = false if value.is_a?(Hash) + inner_hash = true value.each do |inner_key, inner_value| if inner_value.is_a?(Hash) - FlattenHashKeys.new(value).flatten.each do |inner_inner_key, inner_inner_value| - result_hash.merge!("#{key}.#{inner_inner_key}".to_sym => inner_inner_value) - end - else - result_hash.merge!("#{key}.#{inner_key}".to_sym => inner_value) + inner_hash = true end + result_hash.merge!("#{key}.#{inner_key}".to_sym => inner_value) end else result_hash.merge!(key.to_sym => value) end - result_hash + + if inner_hash + FlattenHashKeys.new(result_hash).flatten + else + result_hash + end end end end From 4ae2c6d94b39da6054f55849198bf4e361889d6d Mon Sep 17 00:00:00 2001 From: Leif Gensert Date: Thu, 23 Apr 2015 15:24:59 -0700 Subject: [PATCH 11/16] add spec for nested destination --- spec/lib/morfo/builder_spec.rb | 9 +++++++++ spec/lib/morfo_spec.rb | 10 ++++++++++ 2 files changed, 19 insertions(+) diff --git a/spec/lib/morfo/builder_spec.rb b/spec/lib/morfo/builder_spec.rb index a40be56..a04ec90 100644 --- a/spec/lib/morfo/builder_spec.rb +++ b/spec/lib/morfo/builder_spec.rb @@ -101,5 +101,14 @@ ).build end end + + it_behaves_like "a morfer with nested destination" do + let(:definitions) do + [ + { field: [:tv_show, :title], from: :title }, + { field: [:tv_show, :channel], from: :channel, transformed: "Channel: %{value}" }, + ] + end + end end end diff --git a/spec/lib/morfo_spec.rb b/spec/lib/morfo_spec.rb index 61c95dc..2bd46be 100644 --- a/spec/lib/morfo_spec.rb +++ b/spec/lib/morfo_spec.rb @@ -95,4 +95,14 @@ class WrapperMorfer < Morfo::Base WrapperMorfer end end + + it_behaves_like "a morfer with nested destination" do + subject do + class WrapperMorfer < Morfo::Base + field(:tv_show, :title).from(:title) + field(:tv_show, :channel).from(:channel).transformed {|v| "Channel: #{v}"} + end + WrapperMorfer + end + end end From bec81f7d7b6245452f626ab74183248731ef3dba Mon Sep 17 00:00:00 2001 From: Leif Gensert Date: Thu, 23 Apr 2015 15:39:55 -0700 Subject: [PATCH 12/16] format --- spec/lib/morfo/builder_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/lib/morfo/builder_spec.rb b/spec/lib/morfo/builder_spec.rb index a04ec90..efb4594 100644 --- a/spec/lib/morfo/builder_spec.rb +++ b/spec/lib/morfo/builder_spec.rb @@ -63,7 +63,7 @@ it_behaves_like "a 1 to 1 morfer with transformation" do let(:definitions) do [ - {field: :title, from: :title, transformed: "%{value} and Zombies"} + { field: :title, from: :title, transformed: "%{value} and Zombies" } ] end end From 2d5b39221a4054fd1ff0bbefd844a7c742bbd3c0 Mon Sep 17 00:00:00 2001 From: Leif Gensert Date: Thu, 23 Apr 2015 16:14:12 -0700 Subject: [PATCH 13/16] correct headlines --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 52d1119..b52fa2a 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,7 @@ If you want to have access to nested values, just provide the path to that field # {first_name: "Bruce", last_name: "Wayne"}, # ] -## Transformations +### Transformations It's also possible to transform the value in any way ruby lets you transform a value. just provide a block in the `transformed` method. @@ -94,7 +94,7 @@ It's also possible to transform the value in any way ruby lets you transform a v # {title: "Fifty Shades of Grey and Zombies"}, # ] -## Calculations +### Calculations If the value of your field should be based on multiple fields of the input row, yoy can specify a calculation block via the `calculated` method. As an argument the whole input row is passed in. From 29fefe7dce2ae02e56ab21977fccec85bd7d9b34 Mon Sep 17 00:00:00 2001 From: Leif Gensert Date: Thu, 23 Apr 2015 16:16:52 -0700 Subject: [PATCH 14/16] add ruby syntax highlighting to README --- README.md | 131 +++++++++++++++++++++++++++++------------------------- 1 file changed, 70 insertions(+), 61 deletions(-) diff --git a/README.md b/README.md index b52fa2a..264d365 100644 --- a/README.md +++ b/README.md @@ -32,83 +32,92 @@ Use the `field` method to specify what fields exist and where they will get thei The most basic form is copying the value from another field. - class Title < Morfo::Base - field(:tv_show_title)from(:title) - end +```ruby +class Title < Morfo::Base + field(:tv_show_title)from(:title) +end +``` Afterwards use the `morf` method to morf all hashes in one array to the end result: - Title.morf([ - {title: "The Walking Dead"} , - {title: "Breaking Bad"}, - ]) +```ruby +Title.morf([ + {title: "The Walking Dead"} , + {title: "Breaking Bad"}, + ]) - # [ - # {tv_show_title: "The Walking Dead"}, - # {tv_show_title: "Breaking Bad"}, - # ] +# [ +# {tv_show_title: "The Walking Dead"}, +# {tv_show_title: "Breaking Bad"}, +# ] +``` If you want to have access to nested values, just provide the path to that field comma separated. - - class Name < Morfo::Base - field(:first_name).from(:name, :first) - field(:last_name).from(:name, :last) - end - - Name.morf([ - { - name: { - first: "Clark", - last: "Kent", - }, - }, - { - name: { - first: "Bruce", - last: "Wayne", - }, - }, - ]) - - # [ - # {first_name: "Clark", last_name: "Kent"}, - # {first_name: "Bruce", last_name: "Wayne"}, - # ] +```ruby +class Name < Morfo::Base + field(:first_name).from(:name, :first) + field(:last_name).from(:name, :last) +end + +Name.morf([ + { + name: { + first: "Clark", + last: "Kent", + }, + }, + { + name: { + first: "Bruce", + last: "Wayne", + }, + }, + ]) + +# [ +# {first_name: "Clark", last_name: "Kent"}, +# {first_name: "Bruce", last_name: "Wayne"}, +# ] +``` ### Transformations It's also possible to transform the value in any way ruby lets you transform a value. just provide a block in the `transformed` method. - class AndZombies < Morfo::Base - field(:title).from(title).transformed {|title| "#{title} and Zombies"} - end +```ruby +class AndZombies < Morfo::Base + field(:title).from(title).transformed {|title| "#{title} and Zombies"} +end - AndZombies.morf([ - {title: "Pride and Prejudice"}, - {title: "Fifty Shades of Grey"}, - ]) +AndZombies.morf([ + {title: "Pride and Prejudice"}, + {title: "Fifty Shades of Grey"}, + ]) - # [ - # {title: "Pride and Prejudice and Zombies"}, - # {title: "Fifty Shades of Grey and Zombies"}, - # ] +# [ +# {title: "Pride and Prejudice and Zombies"}, +# {title: "Fifty Shades of Grey and Zombies"}, +# ] +``` ### Calculations If the value of your field should be based on multiple fields of the input row, yoy can specify a calculation block via the `calculated` method. As an argument the whole input row is passed in. - class NameConcatenator < Morfo::Base - field(:name).calculated {|row| "#{row[:first_name]} #{row[:last_name]}"} - field(:status).calculated {"Best Friend"} - end - - NameConcatenator.morf([ - {first_name: "Robin", last_name: "Hood"}, - {first_name: "Sherlock", last_name: "Holmes"}, - ]) - - # [ - # {:name=>"Robin Hood", :status=>"Best Friend"}, - # {:name=>"Sherlock Holmes", :status=>"Best Friend"} - # ] +```ruby +class NameConcatenator < Morfo::Base + field(:name).calculated {|row| "#{row[:first_name]} #{row[:last_name]}"} + field(:status).calculated {"Best Friend"} +end + +NameConcatenator.morf([ + {first_name: "Robin", last_name: "Hood"}, + {first_name: "Sherlock", last_name: "Holmes"}, + ]) + +# [ +# {:name=>"Robin Hood", :status=>"Best Friend"}, +# {:name=>"Sherlock Holmes", :status=>"Best Friend"} +# ] +``` From 0672b4401fcdc4b810ba03acca13d5a16e0b09aa Mon Sep 17 00:00:00 2001 From: Leif Gensert Date: Thu, 23 Apr 2015 16:37:15 -0700 Subject: [PATCH 15/16] add section about builders --- README.md | 107 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) diff --git a/README.md b/README.md index 264d365..2061f80 100644 --- a/README.md +++ b/README.md @@ -121,3 +121,110 @@ NameConcatenator.morf([ # {:name=>"Sherlock Holmes", :status=>"Best Friend"} # ] ``` + +### Builder + +On top of creating transformers with Ruby classes, it is also possible to build transformers with a hash syntax (which could then be serialized as json and stored somewhere else). + +```ruby +morfer = Morfo::Builder.new([ + { field: :first_name, from: [:name, :first] }, + { field: :last_name, from: [:name, :last] }, +]) + +morfer.morf([ + { + name: { + first: "Clark", + last: "Kent", + }, + }, + { + name: { + first: "Bruce", + last: "Wayne", + }, + }, + ]) + +# [ +# { first_name: "Clark", last_name: "Kent" }, +# { first_name: "Bruce", last_name: "Wayne" }, +# ] +``` + +The builder includes all other features such as calculation and transformation + +#### Builder Transformations + +To transform a value, use the placeholder %{value} + +```ruby +class AndZombies < Morfo::Base + field(:title).from(title).transformed {|title| "#{title} and Zombies"} +end + +morfer = Morfo::Builder.new([ + { field: :title, from: :title, transformed: "%{title} and Zombies" }, +]) + +morfer.morf([ + {title: "Pride and Prejudice"}, + {title: "Fifty Shades of Grey"}, + ]) + +# [ +# { title: "Pride and Prejudice and Zombies" }, +# { title: "Fifty Shades of Grey and Zombies" }, +# ] +``` + +#### Builder Calculations + +To get access to the other fields use the [ruby string format syntax](http://ruby-doc.org/core-2.2.0/String.html#method-i-25). + +```ruby +morfer = Morfo::Builder.new([ + { field: :name, calculated: "%{first_name} %{last_name}" }, + { field: :status, calculated: "Best Friend" }, +]) + +morfer.morf([ + {first_name: "Robin", last_name: "Hood"}, + {first_name: "Sherlock", last_name: "Holmes"}, +]) + +# [ +# { name: "Robin Hood", status: "Best Friend" }, +# { name: "Sherlock Holmes", status: "Best Friend" } +# ] +``` + +It's even possible to get access to nested keys, using a dot as separator: + +```ruby +morfer = Morfo::Builder.new([ + { field: :first_name, calculated: "%{name.first} %{name.last}" }, +]) + +morfer.morf([ + { + name: { + first: "Clark", + last: "Kent", + }, + }, + { + name: { + first: "Bruce", + last: "Wayne", + }, + }, + ]) + +# [ +# { name: "Clark Kent" }, +# { name: "Bruce Wayne" }, +# ] +``` + From e5714af41246c0b5935af3183476604e3a2e3dba Mon Sep 17 00:00:00 2001 From: Leif Gensert Date: Thu, 23 Apr 2015 16:45:06 -0700 Subject: [PATCH 16/16] beautify README --- README.md | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 2061f80..f307082 100644 --- a/README.md +++ b/README.md @@ -42,13 +42,13 @@ Afterwards use the `morf` method to morf all hashes in one array to the end resu ```ruby Title.morf([ - {title: "The Walking Dead"} , - {title: "Breaking Bad"}, + { title: "The Walking Dead" }, + { title: "Breaking Bad" }, ]) # [ -# {tv_show_title: "The Walking Dead"}, -# {tv_show_title: "Breaking Bad"}, +# { tv_show_title: "The Walking Dead" }, +# { tv_show_title: "Breaking Bad" }, # ] ``` @@ -76,8 +76,8 @@ Name.morf([ ]) # [ -# {first_name: "Clark", last_name: "Kent"}, -# {first_name: "Bruce", last_name: "Wayne"}, +# { first_name: "Clark", last_name: "Kent" }, +# { first_name: "Bruce", last_name: "Wayne" }, # ] ``` @@ -91,13 +91,13 @@ class AndZombies < Morfo::Base end AndZombies.morf([ - {title: "Pride and Prejudice"}, - {title: "Fifty Shades of Grey"}, + { title: "Pride and Prejudice" }, + { title: "Fifty Shades of Grey" }, ]) # [ -# {title: "Pride and Prejudice and Zombies"}, -# {title: "Fifty Shades of Grey and Zombies"}, +# { title: "Pride and Prejudice and Zombies" }, +# { title: "Fifty Shades of Grey and Zombies" }, # ] ``` @@ -112,13 +112,13 @@ class NameConcatenator < Morfo::Base end NameConcatenator.morf([ - {first_name: "Robin", last_name: "Hood"}, - {first_name: "Sherlock", last_name: "Holmes"}, + { first_name: "Robin", last_name: "Hood" }, + { first_name: "Sherlock", last_name: "Holmes" }, ]) # [ -# {:name=>"Robin Hood", :status=>"Best Friend"}, -# {:name=>"Sherlock Holmes", :status=>"Best Friend"} +# { name: "Robin Hood", status: "Best Friend" }, +# { name: "Sherlock Holmes", status: "Best Friend" } # ] ``` @@ -169,8 +169,8 @@ morfer = Morfo::Builder.new([ ]) morfer.morf([ - {title: "Pride and Prejudice"}, - {title: "Fifty Shades of Grey"}, + { title: "Pride and Prejudice" }, + { title: "Fifty Shades of Grey" }, ]) # [ @@ -190,8 +190,8 @@ morfer = Morfo::Builder.new([ ]) morfer.morf([ - {first_name: "Robin", last_name: "Hood"}, - {first_name: "Sherlock", last_name: "Holmes"}, + { first_name: "Robin", last_name: "Hood" }, + { first_name: "Sherlock", last_name: "Holmes" }, ]) # [ @@ -204,7 +204,7 @@ It's even possible to get access to nested keys, using a dot as separator: ```ruby morfer = Morfo::Builder.new([ - { field: :first_name, calculated: "%{name.first} %{name.last}" }, + { field: :name, calculated: "%{name.first} %{name.last}" }, ]) morfer.morf([ @@ -220,7 +220,7 @@ morfer.morf([ last: "Wayne", }, }, - ]) +]) # [ # { name: "Clark Kent" },