karta 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 231b33f0743a4ef1e663dd75c255b172f99d30f1
4
+ data.tar.gz: 081df11b8bd73cc4ca3c9ba8b52ed98e91b79221
5
+ SHA512:
6
+ metadata.gz: 7dbf32d707430e58f73cdd13fc91420ed11902bd866c361e50713c73a90b883145bd7451738bd57050df219a4c80b96fb08b696145c1b0c0bb6d16a0499b2b44
7
+ data.tar.gz: d694e2de81f5088196ae1b9a56859e36f001c43f77679a6fda2df234010f9658cee79329ec92ecbea20bf84fcc914849362c46f1dacb193090702fb11bbe0c64
data/.gitignore ADDED
@@ -0,0 +1,16 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.bundle
11
+ *.so
12
+ *.o
13
+ *.a
14
+ mkmf.log
15
+ /ideas/
16
+ .byebug_history
data/.reek ADDED
@@ -0,0 +1,3 @@
1
+ exclude_paths:
2
+ - ideas
3
+ - spec
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format Fuubar
2
+ --color
data/.rubocop.yml ADDED
@@ -0,0 +1,3 @@
1
+ AllCops:
2
+ Exclude:
3
+ - 'ideas/**/*'
data/.ruby-gemset ADDED
@@ -0,0 +1 @@
1
+ karta
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 2.3.1
data/.yardopts ADDED
@@ -0,0 +1,3 @@
1
+ lib/**/*.rb
2
+ -
3
+ LICENSE
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+ source 'https://rubygems.org'
3
+
4
+ # Specify your gem's dependencies in karta.gemspec
5
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2016 Samuel Nilsson
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,106 @@
1
+ # Karta
2
+
3
+ Karta is a very light-weight Ruby library that makes it easy to create mapper objects in a Ruby application. The mapper object makes it easy to map or transform one object into an other. The main use case is to transform data objects from an domain to another domain, e.g. map a `Twitter::User` to `User`. Instead of having to, for example, define a method `#from_twitter_user` on the `User` class which sets all attributes correctly a mapper object is created which defines all mappings.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'karta'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install karta
20
+
21
+ ## Usage
22
+
23
+ ### Setting up a mapper
24
+ The main functionality of Karta is used by letting a class inherit from `Karta::Mapper`. By doing so the class gets a `#map` method which will run all methods starting with `map_` with the two objects as arguments.
25
+
26
+ ##### Example
27
+ ```ruby
28
+ class MyTwitterMapper < Karta::Mapper
29
+ def map_email(twitter_user, user)
30
+ user.email = twitter_user.twitter_email
31
+ end
32
+
33
+ def map_username(twitter_user, user)
34
+ user.username = twitter_user.twitter_handle.gsub('@', '')
35
+ end
36
+ end
37
+
38
+ # Mapping from one instance to another, overriding
39
+ # values if already set
40
+ twitter_user = ...
41
+ user = ...
42
+ mapped_user = MyTwitterMapper.new.map(from: twitter_user, to: user)
43
+
44
+ # Mapping from an instance to a class, will return
45
+ # a new instance of the class
46
+ twitter_user = ...
47
+ user = MyTwitterMapper.new.map(from: twitter_user, to: User)
48
+
49
+ # Using class method (which instantiates before running map on the instance)
50
+ twitter_user = ...
51
+ user = MyTwitterMapper.map(from: twitter_user, to: User)
52
+ ```
53
+
54
+ ### Using the mapper registry
55
+ To simplify cases when an application has several mappers it is possible to register mappers and then only use `Karta.map` to map between two objects.
56
+
57
+ ##### Example
58
+
59
+ ```ruby
60
+ class MyMapper < Karta::Mapper
61
+ # map methods...
62
+ end
63
+
64
+ Karta.register_mapper MyMapper, from_klass: Twitter::User, to_klass: User
65
+
66
+ # There is an option to not have to specify from and to class
67
+ # when registering the mapper. This relies on reflection and
68
+ # requires the mapper to have a class name on the correct format
69
+ # (`[from class]To[to class]Mapper`) for example `FooToBarMapper`.
70
+ class FooToBarMapper < Karta::Mapper
71
+ # map methods
72
+ end
73
+
74
+ # Register mapper with from_klass = Foo and to_klass = Bar
75
+ Karta.register_mapper FooToBarMapper
76
+
77
+ # Using Karta.map to map between two objects
78
+ twitter_user = ...
79
+ user = Karta.map(from: twitter_user, to: User)
80
+
81
+ foo = ...
82
+ bar = Karta.map(from: foo, to Bar)
83
+ ```
84
+
85
+ ### One to one mappings
86
+ Sometimes you have fields that could be mapped directly without performing any transformations instead of having to define mapping methods for each you can use `one_to_one_mapping :attr`.
87
+
88
+ ##### Example
89
+
90
+ ```ruby
91
+ class FooToBarMapper < Karta::Mapper
92
+ one_to_one_mapping :email
93
+ end
94
+
95
+ foo = ...
96
+ bar = FooToBarMapper.map(from: foo, to Bar)
97
+ puts foo.email == bar.email # => true
98
+ ```
99
+
100
+ ## Contributing
101
+
102
+ 1. Fork it ( https://github.com/samuel02/karta/fork )
103
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
104
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
105
+ 4. Push to the branch (`git push origin my-new-feature`)
106
+ 5. Create a new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ # frozen_string_literal: true
2
+ require 'bundler/gem_tasks'
data/karta.gemspec ADDED
@@ -0,0 +1,29 @@
1
+ # coding: utf-8
2
+ # frozen_string_literal: true
3
+ lib = File.expand_path('../lib', __FILE__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require 'karta/version'
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = 'karta'
9
+ spec.version = Karta::VERSION
10
+ spec.authors = ['Samuel Nilsson']
11
+ spec.email = ['mail@samuelnilsson.me']
12
+ spec.summary = 'A simple Ruby gem for creating mappers which map one '\
13
+ 'object to another.'
14
+ spec.homepage = ''
15
+ spec.license = 'MIT'
16
+
17
+ spec.files = `git ls-files -z`.split("\x0")
18
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
19
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
20
+ spec.require_paths = ['lib']
21
+
22
+ spec.add_development_dependency 'bundler', '~> 1.7'
23
+ spec.add_development_dependency 'rake', '~> 10.0'
24
+ spec.add_development_dependency 'rspec', '~> 3.5'
25
+ spec.add_development_dependency 'byebug', '~> 9.0'
26
+ spec.add_development_dependency 'rubocop', '~> 0.46.0'
27
+ spec.add_development_dependency 'simplecov', '~> 0.12.0'
28
+ spec.add_development_dependency 'fuubar', '~> 2.2'
29
+ end
data/lib/karta.rb ADDED
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+ require 'karta/version'
3
+ require 'karta/mapper_registry'
4
+ require 'karta/error'
5
+ require 'karta/mapper'
6
+
7
+ # The module that contains everything relating to Karta
8
+ #
9
+ # @author Samuel Nilsson <mail@samuelnilsson.me>
10
+ module Karta
11
+ # Holds the global registry of mappers
12
+ #
13
+ # @return [MapperRegistry] the mapper registry
14
+ def self.mapper_registry
15
+ @mapper_registry ||= MapperRegistry.new
16
+ end
17
+
18
+ # Register a new mapper to the registry
19
+ #
20
+ # @param mapper [Karta::Mapper] the mapper to be registered
21
+ # @param from_klass [Class] the class the mapper is supposed to map from
22
+ # @param to_klass [Class] the class the mapper is supposed to map to
23
+ #
24
+ # @return [MapperRegistry]
25
+ def self.register_mapper(mapper, from_klass: nil, to_klass: nil)
26
+ mapper_registry.register(mapper: mapper,
27
+ from_klass: from_klass,
28
+ to_klass: to_klass)
29
+ end
30
+
31
+ # Map an object to another using a registered mapper. Returns a new instance
32
+ # of the mapped object.
33
+ #
34
+ # @param from [Object] the object to map from
35
+ # @param to [Object, Class] the object or class to map to
36
+ #
37
+ # @raise [ArgumentError] if trying to map from a class rather than an instance
38
+ #
39
+ # @return [Object] a new instance of the same type as `to`
40
+ def self.map(from:, to:)
41
+ to, to_klass, from, from_klass = *_handle_map_args(from, to)
42
+
43
+ mapper_registry.find(from_klass: from_klass, to_klass: to_klass)
44
+ .map(from: from, to: to)
45
+ end
46
+
47
+ # Map an object to another using a registered mapper. Performs the mapping
48
+ # "in place" and thus modifies the 'to' object and overrides attributes.
49
+ #
50
+ # @param from [Object] the object to map from
51
+ # @param to [Object, Class] the object or class to map to
52
+ #
53
+ # @raise [ArgumentError] if trying to map from a class rather than an instance
54
+ #
55
+ # @return [Object] returns modified version of 'to'
56
+ def self.map!(from:, to:)
57
+ to, to_klass, from, from_klass = *_handle_map_args(from, to)
58
+
59
+ mapper_registry.find(from_klass: from_klass, to_klass: to_klass)
60
+ .map!(from: from, to: to)
61
+ end
62
+
63
+ # @api private
64
+ def self._handle_map_args(from, to)
65
+ raise ArgumentError, 'cannot map from a class' if from.is_a?(Class)
66
+ to_klass = to.is_a?(Class) ? to : to.class
67
+ [to, to_klass, from, from.class]
68
+ end
69
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+ module Karta
3
+ # Basic class for exceptions in Karta
4
+ class Error < StandardError; end
5
+
6
+ # Exception for case when a mapper cannot be found
7
+ class MapperNotFoundError < Error
8
+ def initialize(from, to)
9
+ super("no mapper found (#{from} → #{to})")
10
+ end
11
+ end
12
+
13
+ # Exception for case when a mapper has an invalid name
14
+ class InvalidNameError < Error
15
+ def initialize
16
+ super('mapper name must be on format [Foo]To[Bar]Mapper')
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+ module Karta
3
+ # Contains the basic functionality for a mapper object,
4
+ # the class is meant to be inherited by a mapper class
5
+ # in order to get the `#map method as well as being able
6
+ # to specify one-to-one mappings
7
+ class Mapper
8
+ # Maps an object to another using all mapping methods defined on the mapper.
9
+ # Mapping methods are defined as "one-to-one mappings" by using
10
+ # `.one_to_one_mapping`or by defining methods with names starting with
11
+ # 'map_'. The mapping methods are supposed to take the object which it is
12
+ # mapping from as well as the object which it should map into.
13
+ #
14
+ # @param from [Object] the object to map from
15
+ # @param to [Object, Class] the object or class to map to
16
+ #
17
+ # @return [Object] a new instance of the same type as `to`
18
+ def map(from:, to:)
19
+ to_klass = to.is_a?(Class) ? to : to.class
20
+ _map(from, to_klass.new)
21
+ end
22
+
23
+ # Maps an object to another using all mapping methods defined on the mapper.
24
+ # see #map
25
+ #
26
+ # @param from [Object] the object to map from
27
+ # @param to [Object, Class] the object or class to map to
28
+ #
29
+ # @return [Object] a new instance of the same type as `to`
30
+ def map!(from:, to:)
31
+ to = to.new if to.is_a?(Class)
32
+ _map(from, to)
33
+ end
34
+
35
+ # Find all mapping methods defined on the mapper.
36
+ #
37
+ # @return [Array<Symbol>] names of all methods beginning with `map_`
38
+ def self.mapping_methods
39
+ public_instance_methods(true).grep(/^map_/)
40
+ end
41
+
42
+ # Defines a one-to-one mapping on the mapper as a method.
43
+ #
44
+ # A one-to-one-mapping is a mapping where the attribute names are equal
45
+ # and no transformation is supposed to take place. E.g. `foo.id = bar.id`.
46
+ def self.one_to_one_mapping(attr)
47
+ define_method("map_#{attr}") do |from, to|
48
+ to.send("#{attr}=", from.send(attr))
49
+ end
50
+ end
51
+
52
+ # Instantiates a mapper and runs the `#map` method
53
+ #
54
+ # @param from [Object] the object to map from
55
+ # @param to [Object, Class] the object or class to map to
56
+ #
57
+ # @return [Object] a new instance of the same type as `to`
58
+ def self.map(from:, to:)
59
+ new.map(from: from, to: to)
60
+ end
61
+
62
+ # Instantiates a mapper and runs the `#map!` method
63
+ #
64
+ # @param from [Object] the object to map from
65
+ # @param to [Object, Class] the object or class to map to
66
+ #
67
+ # @return [Object] a new instance of the same type as `to`
68
+ def self.map!(from:, to:)
69
+ new.map!(from: from, to: to)
70
+ end
71
+
72
+ private
73
+
74
+ def _map(from, to)
75
+ self.class.mapping_methods.each { |meth| send(meth, from, to) }
76
+ to
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+ module Karta
3
+ # Simple collection class for mappers
4
+ class MapperRegistry
5
+ attr_reader :mappers
6
+
7
+ def initialize
8
+ @mappers = []
9
+ end
10
+
11
+ # Register a mapper to the registry.
12
+ #
13
+ # If no from_klass or to_klass are specified it will try to parse this
14
+ # from the class name of the mapper. This assumes that the class name
15
+ # of the mapper is on the form `[from class]To[to class]Mapper`, e.g.
16
+ # `FooToBarMapper`. For obvious reasons this makes it hard to handle
17
+ # cases where one of the classes is namespaced, e.g. `Baz::Bar`. For
18
+ # those cases the `from_klass` and `to_klass` must be specified.
19
+ #
20
+ # @param mapper [Karta::Mapper] the mapper to register
21
+ # @param from_klass [Class] the class the mapper is supposed to map from
22
+ # @param to_klass [Class] the class the mapper is supposed to map to
23
+ #
24
+ # @raise [InvalidNameError] if no `from_klass` or `to_klass` is specified
25
+ # and the class name of the mapper does not follow the correct format.
26
+ #
27
+ # @return [MapperRegistry] the mapper registry after the mapper has been
28
+ # added
29
+ def register(mapper:, from_klass: nil, to_klass: nil)
30
+ unless from_klass && to_klass
31
+ from_klass, to_klass = *klasses_from_class_name(mapper.to_s)
32
+ end
33
+
34
+ mappers.push(mapper: mapper,
35
+ from_klass: from_klass,
36
+ to_klass: to_klass)
37
+
38
+ self
39
+ end
40
+
41
+ # Find a mapper in the registry
42
+ #
43
+ # @param from_klass [Class] the class the mapper is supposed to map from
44
+ # @param to_klass [Class] the class the mapper is supposed to map to
45
+ #
46
+ # @raise [MapperNotFoundError] if no applicable mapper is found
47
+ #
48
+ # @return [Karta::Mapper] the found mapper
49
+ def find(from_klass:, to_klass:)
50
+ mappers.find(lambda do
51
+ raise MapperNotFoundError.new(from_klass, to_klass)
52
+ end) do |mapper|
53
+ mapper[:from_klass] == from_klass && mapper[:to_klass] == to_klass
54
+ end.fetch(:mapper)
55
+ end
56
+
57
+ private
58
+
59
+ def klasses_from_class_name(klass_name)
60
+ raise InvalidNameError unless klass_name =~ /.*To.*Mapper/
61
+
62
+ klass_name.gsub('Mapper', '')
63
+ .split('To')
64
+ .map(&Kernel.method(:const_get))
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+ module Karta
3
+ VERSION = '1.0.1'
4
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+ require 'spec_helper'
3
+
4
+ describe 'mapping from one instance to a class' do
5
+ before do
6
+ define_klass 'Foo', attrs: [:id, :foo_name]
7
+ define_klass 'Bar', attrs: [:id, :name]
8
+
9
+ define_klass 'FooToBarMapper', base: Karta::Mapper do
10
+ one_to_one_mapping :id
11
+
12
+ def map_name(foo, bar)
13
+ bar.name = foo.foo_name
14
+ end
15
+ end
16
+ end
17
+
18
+ let(:foo) { Foo.new(id: 1, foo_name: 'Foo') }
19
+
20
+ describe 'using a mapper object' do
21
+ subject { FooToBarMapper.map(from: foo, to: Bar) }
22
+
23
+ it 'creates a new instance and maps all fields defined in the mapper' do
24
+ expect(subject.class).to eq Bar
25
+ expect(subject.id).to eq foo.id
26
+ expect(subject.name).to eq foo.foo_name
27
+ end
28
+ end
29
+
30
+ describe 'using the mapper registry' do
31
+ before do
32
+ Karta.register_mapper FooToBarMapper
33
+ end
34
+
35
+ subject { Karta.map(from: foo, to: Bar) }
36
+
37
+ it 'creates a new instance and maps all fields defined in the mapper' do
38
+ expect(subject.class).to eq Bar
39
+ expect(subject.id).to eq foo.id
40
+ expect(subject.name).to eq foo.foo_name
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+ require 'spec_helper'
3
+
4
+ describe 'mapping from one instance to another' do
5
+ before do
6
+ define_klass 'Foo', attrs: [:id, :foo_name]
7
+ define_klass 'Bar', attrs: [:id, :name]
8
+
9
+ define_klass 'FooToBarMapper', base: Karta::Mapper do
10
+ one_to_one_mapping :id
11
+
12
+ def map_name(foo, bar)
13
+ bar.name = foo.foo_name
14
+ end
15
+ end
16
+ end
17
+
18
+ let(:foo) { Foo.new(id: 1, foo_name: 'Foo') }
19
+ let(:bar) { Bar.new(id: 2, name: '') }
20
+
21
+ describe 'using a mapper object' do
22
+ subject { FooToBarMapper.map!(from: foo, to: bar) }
23
+
24
+ it 'maps all fields defined in the mapper' do
25
+ expect(subject.class).to eq Bar
26
+ expect(subject.id).to eq foo.id
27
+ expect(subject.name).to eq foo.foo_name
28
+ expect(bar.id).to eq 1
29
+ expect(bar.name).to eq 'Foo'
30
+ end
31
+ end
32
+
33
+ describe 'using the mapper registry' do
34
+ before do
35
+ Karta.register_mapper FooToBarMapper
36
+ end
37
+
38
+ subject { Karta.map!(from: foo, to: bar) }
39
+
40
+ it 'maps all fields defined in the mapper' do
41
+ expect(subject.class).to eq Bar
42
+ expect(subject.id).to eq foo.id
43
+ expect(subject.name).to eq foo.foo_name
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+ require 'spec_helper'
3
+
4
+ describe Karta::MapperRegistry do
5
+ describe '#register' do
6
+ subject(:mapper_registry) { Karta::MapperRegistry.new }
7
+
8
+ context 'when from_klass or to_klass are not specified' do
9
+ context 'when the class name of the mapper follows the correct format' do
10
+ before do
11
+ define_klass 'Foo'
12
+ define_klass 'Bar'
13
+ define_klass 'FooToBarMapper', base: Karta::Mapper
14
+ end
15
+
16
+ it 'parses the class names from the mapper name and adds the mapper' do
17
+ mapper_registry.register(mapper: FooToBarMapper)
18
+
19
+ expect(mapper_registry.mappers.count).to eq 1
20
+
21
+ mapper = mapper_registry.mappers.first
22
+
23
+ expect(mapper[:mapper]).to eq FooToBarMapper
24
+ expect(mapper[:from_klass]).to eq Foo
25
+ expect(mapper[:to_klass]).to eq Bar
26
+ end
27
+ end
28
+
29
+ context 'when the class name of the mapper contains a namespaced class' do
30
+ before do
31
+ define_klass 'Baz'
32
+ define_klass 'Baz::Foo'
33
+ define_klass 'Bar'
34
+ define_klass 'Baz::FooToBarMapper', base: Karta::Mapper
35
+ end
36
+
37
+ it 'parses the class names from the mapper name and adds the mapper' do
38
+ mapper_registry.register(mapper: Baz::FooToBarMapper)
39
+
40
+ expect(mapper_registry.mappers.count).to eq 1
41
+
42
+ mapper = mapper_registry.mappers.first
43
+
44
+ expect(mapper[:mapper]).to eq Baz::FooToBarMapper
45
+ expect(mapper[:from_klass]).to eq Baz::Foo
46
+ expect(mapper[:to_klass]).to eq Bar
47
+ end
48
+ end
49
+
50
+ context "when the class name of the mapper isn't on the correct format" do
51
+ before do
52
+ define_klass 'CustomMapper', base: Karta::Mapper
53
+ end
54
+
55
+ it 'raises an error' do
56
+ expect do
57
+ mapper_registry.register(mapper: CustomMapper)
58
+ end.to raise_error Karta::InvalidNameError,
59
+ 'mapper name must be on format [Foo]To[Bar]Mapper'
60
+ end
61
+ end
62
+ end
63
+
64
+ context 'when from_klass and to_klass are specified' do
65
+ before do
66
+ define_klass 'Foo'
67
+ define_klass 'Bar'
68
+ define_klass 'CustomMapper', base: Karta::Mapper
69
+ end
70
+
71
+ it 'adds the mapper to the registry' do
72
+ mapper_registry.register mapper: CustomMapper,
73
+ from_klass: Foo,
74
+ to_klass: Bar
75
+
76
+ expect(mapper_registry.mappers.count).to eq 1
77
+
78
+ mapper = mapper_registry.mappers.first
79
+
80
+ expect(mapper[:mapper]).to eq CustomMapper
81
+ expect(mapper[:from_klass]).to eq Foo
82
+ expect(mapper[:to_klass]).to eq Bar
83
+ end
84
+ end
85
+ end
86
+
87
+ describe '#find' do
88
+ subject(:mapper_registry) { Karta::MapperRegistry.new }
89
+
90
+ context 'when an appropriate mapper exists' do
91
+ before do
92
+ define_klass 'Foo'
93
+ define_klass 'Bar'
94
+ define_klass 'FooToBarMapper', base: Karta::Mapper
95
+
96
+ allow(mapper_registry).to\
97
+ receive(:mappers).and_return([{
98
+ mapper: FooToBarMapper,
99
+ from_klass: Foo,
100
+ to_klass: Bar
101
+ }])
102
+ end
103
+
104
+ it 'returns the mapper from the registry' do
105
+ expect(
106
+ mapper_registry.find(from_klass: Foo, to_klass: Bar)
107
+ ).to eq FooToBarMapper
108
+ end
109
+ end
110
+
111
+ context 'when no appropriate mapper can be found' do
112
+ it 'raises an error with a good error message' do
113
+ expect do
114
+ mapper_registry.find(from_klass: Integer, to_klass: Array)
115
+ end.to raise_error Karta::MapperNotFoundError,
116
+ 'no mapper found (Integer → Array)'
117
+ end
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,131 @@
1
+ # frozen_string_literal: true
2
+ require 'spec_helper'
3
+
4
+ describe Karta::Mapper do
5
+ describe '#map' do
6
+ before do
7
+ define_klass 'Foo'
8
+ define_klass 'Bar'
9
+ end
10
+
11
+ let!(:mapper) do
12
+ define_klass('MyMapper', base: Karta::Mapper).new
13
+ end
14
+
15
+ before do
16
+ allow(MyMapper).to receive(:mapping_methods).and_return([:method])
17
+ end
18
+
19
+ context 'when to is an instance' do
20
+ let(:from) { Foo.new }
21
+ let(:to) { Bar.new }
22
+
23
+ it 'calls all mapping methods with from and to \
24
+ and returns new instance' do
25
+ expect(mapper).to receive(:send).with(:method, from, Bar)
26
+ mapper.map(from: from, to: to)
27
+ end
28
+ end
29
+
30
+ context 'when to is a class' do
31
+ let(:from) { Foo.new }
32
+ let(:to) { Bar }
33
+
34
+ it 'calls all mapping methods with from and a to instance' do
35
+ expect(mapper).to receive(:send).with(:method, from, to)
36
+
37
+ mapper.map(from: from, to: to)
38
+ end
39
+ end
40
+ end
41
+
42
+ describe '#map!' do
43
+ before do
44
+ define_klass 'Foo'
45
+ define_klass 'Bar'
46
+ end
47
+
48
+ let!(:mapper) do
49
+ define_klass('MyMapper', base: Karta::Mapper).new
50
+ end
51
+
52
+ before do
53
+ allow(MyMapper).to receive(:mapping_methods).and_return([:method])
54
+ end
55
+
56
+ context 'when to is an instance' do
57
+ let(:from) { Foo.new }
58
+ let(:to) { Bar.new }
59
+
60
+ it 'calls all mapping methods with from and to \
61
+ and modifies to in place' do
62
+ expect(mapper).to receive(:send).with(:method, from, to)
63
+ mapper.map!(from: from, to: to)
64
+ end
65
+ end
66
+
67
+ context 'when to is a class' do
68
+ let(:from) { Foo.new }
69
+ let(:to) { Bar }
70
+
71
+ it 'calls all mapping methods with from and a to instance' do
72
+ expect(mapper).to receive(:send).with(:method, from, to)
73
+
74
+ mapper.map!(from: from, to: to)
75
+ end
76
+ end
77
+ end
78
+
79
+ describe '#mapping_methods' do
80
+ let(:mapper_klass) do
81
+ define_klass 'MyMapper', base: Karta::Mapper do
82
+ def map_foo; end
83
+
84
+ def bar; end
85
+
86
+ private
87
+
88
+ def map_baz; end
89
+ end
90
+ end
91
+
92
+ it 'finds all instance methods that starts with map_' do
93
+ expect(mapper_klass.mapping_methods).to include :map_foo
94
+ expect(mapper_klass.mapping_methods).to_not include :bar
95
+ end
96
+
97
+ it 'skips private methods' do
98
+ expect(mapper_klass.private_method_defined?(:map_baz)).to \
99
+ be true
100
+ expect(mapper_klass.mapping_methods).to_not \
101
+ include :map_baz
102
+ end
103
+ end
104
+
105
+ describe '.one_to_one_mapping' do
106
+ let(:mapper) do
107
+ define_klass 'Foo', base: Karta::Mapper do
108
+ one_to_one_mapping :foo
109
+ end.new
110
+ end
111
+
112
+ it 'defines a mapping method on the instance' do
113
+ expect(mapper).to respond_to :map_foo
114
+ end
115
+ end
116
+
117
+ describe '.map' do
118
+ let(:mapper) { double('mapper') }
119
+ let(:from) { double('from') }
120
+ let(:to) { double('to') }
121
+
122
+ let(:mapper_klass) { define_klass 'Mapper', base: Karta::Mapper }
123
+
124
+ it 'instantiates self and runs the map method on the instance' do
125
+ expect(mapper_klass).to receive(:new).and_return(mapper)
126
+ expect(mapper).to receive(:map).with(from: from, to: to)
127
+
128
+ mapper_klass.map(from: from, to: to)
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,154 @@
1
+ # frozen_string_literal: true
2
+ require 'spec_helper'
3
+
4
+ describe Karta do
5
+ describe '.mapper_registry' do
6
+ context 'when a mapper registry has already been initialized' do
7
+ let(:mapper_registry) { double('mapper_registry') }
8
+
9
+ it 'returns the mapper registry' do
10
+ Karta.instance_variable_set('@mapper_registry', mapper_registry)
11
+ expect(Karta.mapper_registry).to eq mapper_registry
12
+ end
13
+ end
14
+
15
+ context 'when no mapper registry has been initialized' do
16
+ it 'initializes the mapper registry to an empty array' do
17
+ expect(Karta.instance_variable_get('@mapper_registry')).to be_nil
18
+
19
+ Karta.mapper_registry
20
+
21
+ expect(Karta.instance_variable_get('@mapper_registry')).to \
22
+ be_a Karta::MapperRegistry
23
+ end
24
+ end
25
+ end
26
+
27
+ describe '.register_mapper' do
28
+ let(:mapper_registry) { double('mapper_registry') }
29
+ let(:mapper) { double('mapper') }
30
+ let(:from_klass) { double('from_klass') }
31
+ let(:to_klass) { double('to_klass') }
32
+
33
+ before do
34
+ expect(Karta).to receive(:mapper_registry).and_return(mapper_registry)
35
+ end
36
+
37
+ it 'registers a mapper' do
38
+ expect(mapper_registry).to receive(:register)\
39
+ .with(mapper: mapper, from_klass: from_klass, to_klass: to_klass)
40
+
41
+ Karta.register_mapper(mapper, from_klass: from_klass, to_klass: to_klass)
42
+ end
43
+ end
44
+
45
+ describe '.map' do
46
+ context 'when trying to map from a class' do
47
+ let(:foo) { double('foo') }
48
+
49
+ before do
50
+ define_klass 'Bar'
51
+ end
52
+
53
+ it 'raises an ArgumentError' do
54
+ expect do
55
+ Karta.map(from: Bar, to: foo)
56
+ end.to raise_error(ArgumentError, 'cannot map from a class')
57
+ end
58
+ end
59
+
60
+ context 'when mapping to a class' do
61
+ let(:mapper) { double('mapper') }
62
+ let(:mapper_registry) { double('mapper_registry') }
63
+ let(:bar) { double('bar') }
64
+
65
+ before do
66
+ define_klass 'Foo'
67
+ allow(Karta).to receive(:mapper_registry).and_return(mapper_registry)
68
+ end
69
+
70
+ it 'finds a a mapper and maps' do
71
+ expect(mapper_registry).to receive(:find).and_return(mapper)
72
+ expect(mapper).to receive(:map).with(from: bar, to: Foo)
73
+
74
+ Karta.map(from: bar, to: Foo)
75
+ end
76
+ end
77
+
78
+ context 'when mapping to an instance' do
79
+ let(:mapper) { double('mapper') }
80
+ let(:mapper_registry) { double('mapper_registry') }
81
+ let(:bar) { double('bar') }
82
+ let(:foo) { double('foo') }
83
+
84
+ before do
85
+ allow(Karta).to receive(:mapper_registry).and_return(mapper_registry)
86
+ end
87
+
88
+ it 'performs the mapping and returns a new instance' do
89
+ expect(mapper_registry).to receive(:find).and_return(mapper)
90
+ expect(mapper).to receive(:map).with(from: bar, to: foo)
91
+ .and_return(double('new-foo'))
92
+
93
+ result = Karta.map(from: bar, to: foo)
94
+
95
+ expect(result).to_not eq foo
96
+ end
97
+ end
98
+ end
99
+
100
+ describe '.map!' do
101
+ context 'when trying to map from a class' do
102
+ let(:foo) { double('foo') }
103
+
104
+ before do
105
+ define_klass 'Bar'
106
+ end
107
+
108
+ it 'raises an ArgumentError' do
109
+ expect do
110
+ Karta.map!(from: Bar, to: foo)
111
+ end.to raise_error(ArgumentError, 'cannot map from a class')
112
+ end
113
+ end
114
+
115
+ context 'when mapping to a class' do
116
+ let(:mapper) { double('mapper') }
117
+ let(:mapper_registry) { double('mapper_registry') }
118
+ let(:bar) { double('bar') }
119
+
120
+ before do
121
+ define_klass 'Foo'
122
+ allow(Karta).to receive(:mapper_registry).and_return(mapper_registry)
123
+ end
124
+
125
+ it 'finds a a mapper and maps' do
126
+ expect(mapper_registry).to receive(:find).and_return(mapper)
127
+ expect(mapper).to receive(:map!).with(from: bar, to: Foo)
128
+
129
+ Karta.map!(from: bar, to: Foo)
130
+ end
131
+ end
132
+
133
+ context 'when mapping to an instance' do
134
+ let(:mapper) { double('mapper') }
135
+ let(:mapper_registry) { double('mapper_registry') }
136
+ let(:bar) { double('bar') }
137
+ let(:foo) { double('foo') }
138
+
139
+ before do
140
+ allow(Karta).to receive(:mapper_registry).and_return(mapper_registry)
141
+ end
142
+
143
+ it 'performs the mapping and returns the instance' do
144
+ expect(mapper_registry).to receive(:find).and_return(mapper)
145
+ expect(mapper).to receive(:map!).with(from: bar, to: foo)
146
+ .and_return(foo)
147
+
148
+ result = Karta.map!(from: bar, to: foo)
149
+
150
+ expect(result).to eq foo
151
+ end
152
+ end
153
+ end
154
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+ $LOAD_PATH << File.join(File.dirname(__FILE__), '..', 'lib')
3
+ $LOAD_PATH << File.join(File.dirname(__FILE__))
4
+
5
+ require 'simplecov'
6
+ SimpleCov.start
7
+
8
+ require 'rubygems'
9
+ require 'rspec'
10
+ require 'byebug'
11
+
12
+ require 'karta'
13
+
14
+ Dir['spec/support/**/*.rb'].each { |f| require File.expand_path(f) }
15
+
16
+ RSpec.configure do |config|
17
+ config.after do
18
+ Karta.instance_variables.each do |ivar|
19
+ Karta.instance_variable_set(ivar, nil)
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+ module DefineConstantMacros
3
+ # Simple class used for making flexible objects
4
+ # in tests
5
+ class FlexibleObject
6
+ def initialize(data = {})
7
+ data.each { |attr, val| instance_variable_set("@#{attr}", val) }
8
+ end
9
+ end
10
+
11
+ def define_klass(path, attrs: [], base: FlexibleObject, &block)
12
+ namespace, class_name = *constant_path(path)
13
+ klass = Class.new(base)
14
+ namespace.const_set(class_name, klass)
15
+
16
+ klass.class_eval do
17
+ attrs.each { |attr| attr_accessor attr }
18
+ end
19
+
20
+ klass.class_eval(&block) if block_given?
21
+
22
+ @defined_constants << path
23
+ klass
24
+ end
25
+
26
+ def constant_path(constant_name)
27
+ names = constant_name.split('::')
28
+ class_name = names.pop
29
+ namespace = names.inject(Object) { |acc, elem| acc.const_get(elem) }
30
+ [namespace, class_name]
31
+ end
32
+
33
+ def default_constants
34
+ @defined_constants ||= []
35
+ end
36
+
37
+ def clear_generated_constants
38
+ @defined_constants.reverse.each do |path|
39
+ namespace, class_name = *constant_path(path)
40
+ namespace.send(:remove_const, class_name)
41
+ end
42
+
43
+ @defined_constants.clear
44
+ end
45
+
46
+ RSpec.configure do |config|
47
+ config.include DefineConstantMacros
48
+
49
+ config.before do
50
+ default_constants
51
+ end
52
+
53
+ config.after do
54
+ clear_generated_constants
55
+ end
56
+ end
57
+ end
metadata ADDED
@@ -0,0 +1,173 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: karta
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Samuel Nilsson
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-12-06 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.7'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.7'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.5'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.5'
55
+ - !ruby/object:Gem::Dependency
56
+ name: byebug
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '9.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '9.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rubocop
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: 0.46.0
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: 0.46.0
83
+ - !ruby/object:Gem::Dependency
84
+ name: simplecov
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: 0.12.0
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: 0.12.0
97
+ - !ruby/object:Gem::Dependency
98
+ name: fuubar
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '2.2'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '2.2'
111
+ description:
112
+ email:
113
+ - mail@samuelnilsson.me
114
+ executables: []
115
+ extensions: []
116
+ extra_rdoc_files: []
117
+ files:
118
+ - ".gitignore"
119
+ - ".reek"
120
+ - ".rspec"
121
+ - ".rubocop.yml"
122
+ - ".ruby-gemset"
123
+ - ".ruby-version"
124
+ - ".yardopts"
125
+ - Gemfile
126
+ - LICENSE
127
+ - README.md
128
+ - Rakefile
129
+ - karta.gemspec
130
+ - lib/karta.rb
131
+ - lib/karta/error.rb
132
+ - lib/karta/mapper.rb
133
+ - lib/karta/mapper_registry.rb
134
+ - lib/karta/version.rb
135
+ - spec/acceptance/map_instance_to_class_spec.rb
136
+ - spec/acceptance/map_instance_to_instance_spec.rb
137
+ - spec/karta/mapper_registry_spec.rb
138
+ - spec/karta/mapper_spec.rb
139
+ - spec/karta_spec.rb
140
+ - spec/spec_helper.rb
141
+ - spec/support/macros/define_constant.rb
142
+ homepage: ''
143
+ licenses:
144
+ - MIT
145
+ metadata: {}
146
+ post_install_message:
147
+ rdoc_options: []
148
+ require_paths:
149
+ - lib
150
+ required_ruby_version: !ruby/object:Gem::Requirement
151
+ requirements:
152
+ - - ">="
153
+ - !ruby/object:Gem::Version
154
+ version: '0'
155
+ required_rubygems_version: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - ">="
158
+ - !ruby/object:Gem::Version
159
+ version: '0'
160
+ requirements: []
161
+ rubyforge_project:
162
+ rubygems_version: 2.5.1
163
+ signing_key:
164
+ specification_version: 4
165
+ summary: A simple Ruby gem for creating mappers which map one object to another.
166
+ test_files:
167
+ - spec/acceptance/map_instance_to_class_spec.rb
168
+ - spec/acceptance/map_instance_to_instance_spec.rb
169
+ - spec/karta/mapper_registry_spec.rb
170
+ - spec/karta/mapper_spec.rb
171
+ - spec/karta_spec.rb
172
+ - spec/spec_helper.rb
173
+ - spec/support/macros/define_constant.rb