From 625bf08e9915e71a410a36548b3669b673a34fe8 Mon Sep 17 00:00:00 2001 From: Leif Gensert Date: Sun, 12 Jan 2014 23:41:49 +0100 Subject: [PATCH 1/7] change interface to declarative --- lib/morfo.rb | 4 ++-- spec/lib/morfo_spec.rb | 18 ++++++++---------- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/lib/morfo.rb b/lib/morfo.rb index 454d1d6..84a4503 100644 --- a/lib/morfo.rb +++ b/lib/morfo.rb @@ -2,8 +2,8 @@ module Morfo class Base - def self.map from, to, &transformation - mapping_actions << MapAction.new(from, to, transformation) + def self.field field_name, definition + mapping_actions << MapAction.new(definition[:from], field_name, definition[:transformation]) end def self.morf input diff --git a/spec/lib/morfo_spec.rb b/spec/lib/morfo_spec.rb index 3ccb982..41018c4 100644 --- a/spec/lib/morfo_spec.rb +++ b/spec/lib/morfo_spec.rb @@ -34,7 +34,7 @@ context '1 to 1 conversion' do subject do class TitleMapper < Morfo::Base - map :title, :tv_show_title + field :tv_show_title, from: :title end TitleMapper end @@ -54,9 +54,7 @@ class TitleMapper < Morfo::Base context '1 to 1 conversion with transformation' do subject do class NumCastMapper < Morfo::Base - map :cast, :cast_num do |cast| - cast.size - end + field :cast_num, from: :cast, transformation: proc { |v| v.size} end NumCastMapper end @@ -70,8 +68,8 @@ class NumCastMapper < Morfo::Base context '1 to many conversion' do subject do class MutliTitleMapper < Morfo::Base - map :title, :title - map :title, :also_title + field :title, from: :title + field :also_title, from: :title end MutliTitleMapper end @@ -86,14 +84,14 @@ class MutliTitleMapper < Morfo::Base context 'nested source' do subject(:valid_path) do class ImdbRatingMapper < Morfo::Base - map [:ratings, :imdb], :rating + field :rating, from: [:ratings, :imdb] end ImdbRatingMapper end subject(:invalid_path) do class InvalidImdbRatingMapper < Morfo::Base - map [:very, :long, :path, :that, :might, :not, :exist], :rating + field :rating, from: [:very, :long, :path, :that, :might, :not, :exist] end InvalidImdbRatingMapper end @@ -112,8 +110,8 @@ class InvalidImdbRatingMapper < Morfo::Base context 'nested destination' do subject do class WrapperMapper < Morfo::Base - map :title, [:tv_show, :title] - map :channel, [:tv_show, :channel] + field([:tv_show, :title], from: :title) + field([:tv_show, :channel], from: :channel) end WrapperMapper end From 94f1a0709aa4e9e664090289e10350ad349ec495 Mon Sep 17 00:00:00 2001 From: Leif Gensert Date: Sun, 12 Jan 2014 23:51:47 +0100 Subject: [PATCH 2/7] adapt benchmarks --- benchmarks/data.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/benchmarks/data.rb b/benchmarks/data.rb index 8a2e649..520be32 100644 --- a/benchmarks/data.rb +++ b/benchmarks/data.rb @@ -46,20 +46,20 @@ def stringify_keys hash class SimpleMappingSymbol < Morfo::Base BenchmarkData.row.keys.each do |field| - map field, :"#{field}_mapped" + field(:"#{field}_mapped", from: field) end end class SimpleMappingString < Morfo::Base BenchmarkData.row_string_keys.keys.each do |field| - map field, "#{field}_mapped" + field("#{field}_mapped", from: field) end end class NestedMappingSymbol < Morfo::Base BenchmarkData.row_nested.each do |key, value| value.keys.each do |field| - map [key, field], :"#{field}_mapped" + field(:"#{field}_mapped", from: [key, field]) end end end @@ -67,7 +67,7 @@ class NestedMappingSymbol < Morfo::Base class NestedMappingString < Morfo::Base BenchmarkData.row_nested_string_keys.each do |key, value| value.keys.each do |field| - map [key, field], "#{field}_mapped" + field("#{field}_mapped", from: [key, field]) end end end From f5f94b11f362388aa2ea6992e5596bacfb122d33 Mon Sep 17 00:00:00 2001 From: Leif Gensert Date: Sun, 12 Jan 2014 23:58:20 +0100 Subject: [PATCH 3/7] add validation for field --- lib/morfo.rb | 4 ++++ spec/lib/morfo_spec.rb | 12 ++++++++++++ 2 files changed, 16 insertions(+) diff --git a/lib/morfo.rb b/lib/morfo.rb index 84a4503..d90cef1 100644 --- a/lib/morfo.rb +++ b/lib/morfo.rb @@ -3,6 +3,10 @@ module Morfo class Base def self.field field_name, definition + raise( + ArgumentError, + "No field to map from is specified for #{field_name.inspect}" + ) unless definition[:from] mapping_actions << MapAction.new(definition[:from], field_name, definition[:transformation]) end diff --git a/spec/lib/morfo_spec.rb b/spec/lib/morfo_spec.rb index 41018c4..3dd1ca1 100644 --- a/spec/lib/morfo_spec.rb +++ b/spec/lib/morfo_spec.rb @@ -31,6 +31,18 @@ end describe '#morf' do + context 'errors' do + subject(:no_from) do + class NilMapper < Morfo::Base + field :my_field, {} + end + NilMapper + end + it 'raises error for nil field' do + expect{no_from.morf([])}.to raise_error(ArgumentError) + end + end + context '1 to 1 conversion' do subject do class TitleMapper < Morfo::Base From 8293c67cca05d5df197e64bf8178fa61b64a2756 Mon Sep 17 00:00:00 2001 From: Leif Gensert Date: Mon, 13 Jan 2014 00:15:08 +0100 Subject: [PATCH 4/7] implement calculations --- lib/morfo.rb | 28 +++++++++++++++++++++++----- spec/lib/morfo_spec.rb | 18 ++++++++++++++++++ 2 files changed, 41 insertions(+), 5 deletions(-) diff --git a/lib/morfo.rb b/lib/morfo.rb index d90cef1..cb7e81d 100644 --- a/lib/morfo.rb +++ b/lib/morfo.rb @@ -3,11 +3,15 @@ module Morfo class Base def self.field field_name, definition - raise( - ArgumentError, - "No field to map from is specified for #{field_name.inspect}" - ) unless definition[:from] - mapping_actions << MapAction.new(definition[:from], field_name, definition[:transformation]) + if definition[:calculation] + mapping_actions << TransformationAction.new(field_name, definition[:calculation]) + else + raise( + ArgumentError, + "No field to map from is specified for #{field_name.inspect}" + ) unless definition[:from] + mapping_actions << MapAction.new(definition[:from], field_name, definition[:transformation]) + end end def self.morf input @@ -36,6 +40,20 @@ def self.deep_merge! hash, other_hash, &block end end + class TransformationAction + attr_reader :to + attr_reader :calculation + + def initialize to, calculation + @to = to + @calculation = calculation + end + + def execute row + {to => calculation.call(row)} + end + end + class MapAction attr_reader :from attr_reader :to diff --git a/spec/lib/morfo_spec.rb b/spec/lib/morfo_spec.rb index 3dd1ca1..b00e74b 100644 --- a/spec/lib/morfo_spec.rb +++ b/spec/lib/morfo_spec.rb @@ -141,5 +141,23 @@ class WrapperMapper < Morfo::Base end end end + + context 'calculations' do + subject do + class TitlePrefixMapper < Morfo::Base + field :title_with_channel, calculation: proc{|v| "#{v[:title]}, (#{v[:channel]})"} + end + TitlePrefixMapper + end + + it 'maps calculation correctly' do + expected_output = input.map{|v| + { + title_with_channel: "#{v[:title]}, (#{v[:channel]})" + } + } + expect(subject.morf(input)).to eq(expected_output) + end + end end end From d6a15e89454d7f025c7240bef3d8e1d9b84e9c78 Mon Sep 17 00:00:00 2001 From: Leif Gensert Date: Mon, 13 Jan 2014 11:03:50 +0100 Subject: [PATCH 5/7] change interface for calculations and transformations --- lib/morfo.rb | 61 ++++----------------------------------- lib/morfo/actions.rb | 56 +++++++++++++++++++++++++++++++++++ lib/morfo/actions/base.rb | 0 spec/lib/morfo_spec.rb | 24 +++++++++++---- 4 files changed, 79 insertions(+), 62 deletions(-) create mode 100644 lib/morfo/actions.rb create mode 100644 lib/morfo/actions/base.rb diff --git a/lib/morfo.rb b/lib/morfo.rb index cb7e81d..538dbba 100644 --- a/lib/morfo.rb +++ b/lib/morfo.rb @@ -1,16 +1,17 @@ require 'morfo/version' +require 'morfo/actions' module Morfo class Base - def self.field field_name, definition - if definition[:calculation] - mapping_actions << TransformationAction.new(field_name, definition[:calculation]) + def self.field field_name, definition={}, &blk + if blk + mapping_actions << Morfo::Actions::TransformationAction.new(definition[:from], field_name, blk) else raise( ArgumentError, "No field to map from is specified for #{field_name.inspect}" ) unless definition[:from] - mapping_actions << MapAction.new(definition[:from], field_name, definition[:transformation]) + mapping_actions << Morfo::Actions::MapAction.new(definition[:from], field_name) end end @@ -39,56 +40,4 @@ def self.deep_merge! hash, other_hash, &block hash end end - - class TransformationAction - attr_reader :to - attr_reader :calculation - - def initialize to, calculation - @to = to - @calculation = calculation - end - - def execute row - {to => calculation.call(row)} - end - end - - class MapAction - attr_reader :from - attr_reader :to - attr_reader :transformation - - def initialize from, to, transformation - @from = from - @to = to - @transformation = transformation - end - - def execute row - resulting_value = apply_transformation(extract_value(row)) - resulting_value ? store_value(to, resulting_value) : {} - end - - private - def extract_value row - Array(from).inject(row) do |resulting_value, key| - resulting_value ? resulting_value[key] : nil - end - end - - def apply_transformation row - transformation ? transformation.call(row) : row - end - - def store_value to, value - Array(to).reverse.inject({}) do |hash, key| - if hash.keys.first.nil? - hash.merge!(key => value) - else - { key => hash } - end - end - end - end end diff --git a/lib/morfo/actions.rb b/lib/morfo/actions.rb new file mode 100644 index 0000000..d7484e8 --- /dev/null +++ b/lib/morfo/actions.rb @@ -0,0 +1,56 @@ +module Morfo + module Actions + module ValueMethods + def extract_value from, row + Array(from).inject(row) do |resulting_value, key| + resulting_value ? resulting_value[key] : nil + end + end + + def store_value to, value + return {} if value.nil? + + Array(to).reverse.inject({}) do |hash, key| + if hash.keys.first.nil? + hash.merge!(key => value) + else + { key => hash } + end + end + end + end + + class MapAction + include ValueMethods + attr_reader :from + attr_reader :to + + def initialize from, to + @from = from + @to = to + end + + def execute row + store_value(to, extract_value(from, row)) + end + end + + class TransformationAction + include ValueMethods + attr_reader :to + attr_reader :from + attr_reader :transformation + + def initialize from, to, transformation + @from = from + @to = to + @transformation = transformation + end + + def execute row + resulting_value = from ? extract_value(from, row) : nil + store_value(to, transformation.call(resulting_value,row)) + end + end + end +end \ No newline at end of file diff --git a/lib/morfo/actions/base.rb b/lib/morfo/actions/base.rb new file mode 100644 index 0000000..e69de29 diff --git a/spec/lib/morfo_spec.rb b/spec/lib/morfo_spec.rb index b00e74b..a45142b 100644 --- a/spec/lib/morfo_spec.rb +++ b/spec/lib/morfo_spec.rb @@ -66,7 +66,7 @@ class TitleMapper < Morfo::Base context '1 to 1 conversion with transformation' do subject do class NumCastMapper < Morfo::Base - field :cast_num, from: :cast, transformation: proc { |v| v.size} + field(:cast_num, from: :cast){|v,r| v.size} end NumCastMapper end @@ -101,6 +101,13 @@ class ImdbRatingMapper < Morfo::Base ImdbRatingMapper end + subject(:valid_path_with_transformation) do + class ImdbRatingMapper < Morfo::Base + field(:rating, from: [:ratings, :imdb]){|v| "Rating: #{v}"} + end + ImdbRatingMapper + end + subject(:invalid_path) do class InvalidImdbRatingMapper < Morfo::Base field :rating, from: [:very, :long, :path, :that, :might, :not, :exist] @@ -113,6 +120,11 @@ class InvalidImdbRatingMapper < Morfo::Base 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) @@ -123,7 +135,7 @@ class InvalidImdbRatingMapper < Morfo::Base subject do class WrapperMapper < Morfo::Base field([:tv_show, :title], from: :title) - field([:tv_show, :channel], from: :channel) + field([:tv_show, :channel], from: :channel){|v| "Channel: #{v}"} end WrapperMapper end @@ -133,7 +145,7 @@ class WrapperMapper < Morfo::Base { tv_show: { title: v[:title], - channel: v[:channel], + channel: "Channel: #{v[:channel]}", } } } @@ -145,15 +157,15 @@ class WrapperMapper < Morfo::Base context 'calculations' do subject do class TitlePrefixMapper < Morfo::Base - field :title_with_channel, calculation: proc{|v| "#{v[:title]}, (#{v[:channel]})"} + field(:title_with_channel){|v,r| "#{r[:title]}, (#{r[:channel]})"} end TitlePrefixMapper end it 'maps calculation correctly' do - expected_output = input.map{|v| + expected_output = input.map{|r| { - title_with_channel: "#{v[:title]}, (#{v[:channel]})" + title_with_channel: "#{r[:title]}, (#{r[:channel]})" } } expect(subject.morf(input)).to eq(expected_output) From 943ae94dd1e97fb00351e4d4edb56fb979aafa17 Mon Sep 17 00:00:00 2001 From: Leif Gensert Date: Mon, 13 Jan 2014 11:18:24 +0100 Subject: [PATCH 6/7] add test for static value --- spec/lib/morfo_spec.rb | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/spec/lib/morfo_spec.rb b/spec/lib/morfo_spec.rb index a45142b..e92854f 100644 --- a/spec/lib/morfo_spec.rb +++ b/spec/lib/morfo_spec.rb @@ -171,5 +171,19 @@ class TitlePrefixMapper < Morfo::Base expect(subject.morf(input)).to eq(expected_output) end end + + context 'static values' do + subject do + class StaticTitleMapper < Morfo::Base + field(:new_title){ 'Static Title' } + end + StaticTitleMapper + end + + 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 end end From 6735610e8680a4ccd4e15fb4714fe10491bba53c Mon Sep 17 00:00:00 2001 From: Leif Gensert Date: Mon, 13 Jan 2014 11:46:02 +0100 Subject: [PATCH 7/7] change README to new syntax --- README.md | 93 ++++++++++++++++++++++--------------------------------- 1 file changed, 37 insertions(+), 56 deletions(-) diff --git a/README.md b/README.md index c1cb2dd..6203126 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,7 @@ [![Build Status](https://travis-ci.org/leifg/morfo.png?branch=master)](https://travis-ci.org/leifg/morfo) [![Coverage Status](https://coveralls.io/repos/leifg/morfo/badge.png?branch=master)](https://coveralls.io/r/leifg/morfo) [![Code Climate](https://codeclimate.com/github/leifg/morfo.png)](https://codeclimate.com/github/leifg/morfo) [![Dependency Status](https://gemnasium.com/leifg/morfo.png)](https://gemnasium.com/leifg/morfo) [![Gem Version](https://badge.fury.io/rb/morfo.png)](http://badge.fury.io/rb/morfo) -This Gem is inspired by the [active_importer](https://github.com/continuum/active_importer) Gem. - -But instead of importing spreadsheets into models, you can morf (typo intended) arrays of Hashes into other arrays of hashes. +This gem acts like a universal converter from hashes into other hashes. You just define where your hash should get its data from and morfo will do the rest for you. ## Compatibility @@ -28,10 +26,14 @@ Or install it yourself as: In order to morf the hashes you have to provide a class that extends `Morf::Base` -Use the `map` method to specify what field you map to another field: +Use the `field` method to specify what fields exist and where they will get their data from: + +### Simple Mapping + +The most basic form is, just define another field from the input hash. The value will just be copied. class Title < Morfo::Base - map :title, :tv_show_title + field :tv_show_title, from: :title end Afterwards use the `morf` method to morf all hashes in one array to the end result: @@ -46,50 +48,12 @@ Afterwards use the `morf` method to morf all hashes in one array to the end resu # {tv_show_title: 'Breaking Bad'}, # ] -It is also possible to map fields to multiple other fields - - class MultiTitle < Morfo::Base - map :title, :tv_show_title - map :title, :show_title - end - - MultiTitle.morf([ - {title: 'The Walking Dead'} , - {title: 'Breaking Bad'}, - ]) - - # [ - # {tv_show_title: 'The Walking Dead', show_title: 'The Walking Dead'}, - # {tv_show_title: 'Breaking Bad', show_title: 'Breaking Bad'}, - # ] - -## Transformations - -For each mapping you can define a block, that will be called on every input: - - class AndZombies < Morfo::Base - map :title, :title do |title| - "#{title} and Zombies" - end - end - - AndZombies.morf([ - {title: 'Pride and Prejudice'}, - {title: 'Fifty Shades of Grey'}, - ]) - - # [ - # {title: 'Pride and Prejudice and Zombies'}, - # {title: 'Fifty Shades of Grey and Zombies'}, - # ] - -## Nested Values +If you want to have access to nested values, you'll have to provide an array as the key: -You can directly access nested values in the hashes: class Name < Morfo::Base - map [:name, :first], :first_name - map [:name, :last], :last_name + field :first_name, from: [:name, :first] + field :last_name, from: [:name, :last] end Name.morf([ @@ -112,22 +76,39 @@ You can directly access nested values in the hashes: # {first_name: 'Bruce',last_name: 'Wayne'}, # ] +## Transformations -It is also possible to store values in a nested hash: +Every field can also take a transformation block, so that the original input can be transformed. - class Wrapper < Morfo::Base - map :first_name, [:superhero, :name, :first] - map :last_name, [:superhero, :name, :last] + class AndZombies < Morfo::Base + field(:title, from: :title) {|title| "#{title} and Zombies"} end - Name.morf([ - {first_name: 'Clark',last_name: 'Kent'}, - {first_name: 'Bruce',last_name: 'Wayne'},, - ]) + AndZombies.morf([ + {title: 'Pride and Prejudice'}, + {title: 'Fifty Shades of Grey'}, + ]) + + # [ + # {title: 'Pride and Prejudice and Zombies'}, + # {title: 'Fifty Shades of Grey and Zombies'}, + # ] + +As the second argument, the whole row is passed into the block. So you can even do transformation based on the whole row. Or you can leave out all the arguments and return a static value. + + class NameConcatenator < Morfo::Base + field(:name) {|_, row| "#{row[:first_name]} #{row[:last_name]}"} + field(:status) { 'Best Friend' } + end + + NameConcatenator.morf([ + {first_name: 'Robin', last_name: 'Hood'}, + {first_name: 'Sherlock', last_name: 'Holmes'}, + ]) # [ - # { superhero: {name: { first: 'Clark', last: 'Kent'}}}, - # { superhero: {name: { first: 'Bruce', last: 'Wayne'}}}, + # {:name=>"Robin Hood", :status=>"Best Friend"}, + # {:name=>"Sherlock Holmes", :status=>'Best Friend'} # ]