spine-mappings 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +36 -0
- data/.rspec +3 -0
- data/CHANGELOG.md +6 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +42 -0
- data/LICENSE +12 -0
- data/README.md +56 -0
- data/Rakefile +1 -0
- data/lib/spine/mappings.rb +30 -0
- data/lib/spine/mappings/commands/action_fetch.rb +17 -0
- data/lib/spine/mappings/commands/fetch.rb +23 -0
- data/lib/spine/mappings/commands/intercept.rb +24 -0
- data/lib/spine/mappings/mapper.rb +26 -0
- data/lib/spine/mappings/mapping.rb +40 -0
- data/lib/spine/mappings/repository.rb +72 -0
- data/lib/spine/mappings/strategies.rb +28 -0
- data/lib/spine/mappings/strategies/compact.rb +17 -0
- data/lib/spine/mappings/strategies/strict.rb +16 -0
- data/lib/spine/mappings/syntax/any.rb +11 -0
- data/lib/spine/mappings/syntax/boolean.rb +15 -0
- data/lib/spine/mappings/syntax/collection.rb +17 -0
- data/lib/spine/mappings/syntax/date.rb +17 -0
- data/lib/spine/mappings/syntax/datetime.rb +17 -0
- data/lib/spine/mappings/syntax/decimal.rb +15 -0
- data/lib/spine/mappings/syntax/integer.rb +15 -0
- data/lib/spine/mappings/syntax/nested.rb +17 -0
- data/lib/spine/mappings/syntax/registered_mapper.rb +20 -0
- data/lib/spine/mappings/syntax/string.rb +15 -0
- data/lib/spine/mappings/syntax/timestamp.rb +17 -0
- data/lib/spine/mappings/version.rb +5 -0
- data/spec/config/coverage.rb +4 -0
- data/spec/config/default.rb +20 -0
- data/spec/spine/mappings/mappings_spec.rb +263 -0
- data/spine-mappings.gemspec +24 -0
- metadata +137 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: db444936faae00c79848292520913fe94090e6bb
|
4
|
+
data.tar.gz: 77202967f3bb1c391c7ed948ddcc10072efc5242
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 8a0d0e05558ec4e7df7f4ab725186f99b5e2d05e88a703471853e88a47630de7497fb80af14f34eea275863b6f82fe1675583e23e1e319653532cb36349d599a
|
7
|
+
data.tar.gz: 094f222f822677d16b0499ea781c7f662c07b2c72ceb7a9e024ab2b718b93a1e28f6e900dc47158673e834ae3aafe5cf2c00fc914d0f6d7e2aa7482e588a5055
|
data/.gitignore
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
*.gem
|
2
|
+
*.rbc
|
3
|
+
/.config
|
4
|
+
/coverage/
|
5
|
+
/InstalledFiles
|
6
|
+
/pkg/
|
7
|
+
/spec/reports/
|
8
|
+
/test/tmp/
|
9
|
+
/test/version_tmp/
|
10
|
+
/tmp/
|
11
|
+
|
12
|
+
## Specific to RubyMotion:
|
13
|
+
.dat*
|
14
|
+
.repl_history
|
15
|
+
build/
|
16
|
+
|
17
|
+
## Documentation cache and generated files:
|
18
|
+
/.yardoc/
|
19
|
+
/_yardoc/
|
20
|
+
/doc/
|
21
|
+
/rdoc/
|
22
|
+
|
23
|
+
## Environment normalisation:
|
24
|
+
/bin/
|
25
|
+
/.bundle/
|
26
|
+
/vendor/bundle
|
27
|
+
/lib/bundler/man/
|
28
|
+
|
29
|
+
# for a library or gem, you might want to ignore these files since the code is
|
30
|
+
# intended to run in multiple environments; otherwise, check them in:
|
31
|
+
# Gemfile.lock
|
32
|
+
# .ruby-version
|
33
|
+
# .ruby-gemset
|
34
|
+
|
35
|
+
# unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
|
36
|
+
.rvmrc
|
data/.rspec
ADDED
data/CHANGELOG.md
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
spine-mappings (0.1.0)
|
5
|
+
|
6
|
+
GEM
|
7
|
+
remote: https://rubygems.org/
|
8
|
+
specs:
|
9
|
+
codeclimate-test-reporter (0.4.7)
|
10
|
+
simplecov (>= 0.7.1, < 1.0.0)
|
11
|
+
diff-lcs (1.2.5)
|
12
|
+
docile (1.1.5)
|
13
|
+
json (1.8.2)
|
14
|
+
rake (10.4.2)
|
15
|
+
rspec (3.2.0)
|
16
|
+
rspec-core (~> 3.2.0)
|
17
|
+
rspec-expectations (~> 3.2.0)
|
18
|
+
rspec-mocks (~> 3.2.0)
|
19
|
+
rspec-core (3.2.3)
|
20
|
+
rspec-support (~> 3.2.0)
|
21
|
+
rspec-expectations (3.2.1)
|
22
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
23
|
+
rspec-support (~> 3.2.0)
|
24
|
+
rspec-mocks (3.2.1)
|
25
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
26
|
+
rspec-support (~> 3.2.0)
|
27
|
+
rspec-support (3.2.2)
|
28
|
+
simplecov (0.10.0)
|
29
|
+
docile (~> 1.1.0)
|
30
|
+
json (~> 1.8)
|
31
|
+
simplecov-html (~> 0.10.0)
|
32
|
+
simplecov-html (0.10.0)
|
33
|
+
|
34
|
+
PLATFORMS
|
35
|
+
ruby
|
36
|
+
|
37
|
+
DEPENDENCIES
|
38
|
+
bundler (~> 1.7)
|
39
|
+
codeclimate-test-reporter (~> 0.4)
|
40
|
+
rake (~> 10.0)
|
41
|
+
rspec (~> 3.2)
|
42
|
+
spine-mappings!
|
data/LICENSE
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
Copyright (c) 2015, TOGGL LLC
|
2
|
+
All rights reserved.
|
3
|
+
|
4
|
+
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
5
|
+
|
6
|
+
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
7
|
+
|
8
|
+
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
9
|
+
|
10
|
+
3. Neither the name of the TOGGL LLC nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
|
11
|
+
|
12
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
data/README.md
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
# Spine::Mappings
|
2
|
+
|
3
|
+
[![Gem Version](https://badge.fury.io/rb/spine-mappings.svg)](http://badge.fury.io/rb/spine-mappings)
|
4
|
+
[![Dependency Status](https://gemnasium.com/rspine/mappings.svg)](https://gemnasium.com/rspine/mappings)
|
5
|
+
[![Test Coverage](https://codeclimate.com/github/rspine/mappings/badges/coverage.svg)](https://codeclimate.com/github/rspine/mappings/coverage)
|
6
|
+
[![Code Climate](https://codeclimate.com/github/rspine/mappings/badges/gpa.svg)](https://codeclimate.com/github/rspine/mappings)
|
7
|
+
[![Codeship Status for rspine/mappings](https://codeship.com/projects/90a03900-e41c-0132-2cdb-16773c71d38d/status?branch=master)](https://codeship.com/projects/81788)
|
8
|
+
|
9
|
+
Maps object to hashes using DSL. You can give different namespaces for mappings
|
10
|
+
by extending module or class with `Spine::Mappings::Repository`.
|
11
|
+
|
12
|
+
## Installation
|
13
|
+
|
14
|
+
To install it, add the gem to your Gemfile:
|
15
|
+
|
16
|
+
```ruby
|
17
|
+
gem 'spine-mappings'
|
18
|
+
```
|
19
|
+
|
20
|
+
Then run `bundle`. If you're not using Bundler, just `gem install spine-mappings`.
|
21
|
+
|
22
|
+
## Usage
|
23
|
+
|
24
|
+
```ruby
|
25
|
+
Spine::Mappings.define :tag do
|
26
|
+
integer :id, from: identity
|
27
|
+
string :name
|
28
|
+
end
|
29
|
+
|
30
|
+
Spine::Mappings.define :product do
|
31
|
+
integer :id, from: :identity
|
32
|
+
string :name
|
33
|
+
decimal :price
|
34
|
+
boolean :is_available, { |source| source.available? }
|
35
|
+
date :available_from
|
36
|
+
date :available_until
|
37
|
+
tag :tags, nullable: true
|
38
|
+
end
|
39
|
+
|
40
|
+
Spine::Mapings.define :timestamps do
|
41
|
+
timestamp :created_at
|
42
|
+
timestamp :updated_at
|
43
|
+
end
|
44
|
+
|
45
|
+
Spine::Mappings.find(:product, :timestamps).map(product)
|
46
|
+
|
47
|
+
Spine::Mappings.find(:product, :timestamps).map_all(products)
|
48
|
+
```
|
49
|
+
|
50
|
+
You can provide also `comapct` or `strict` strategy for mapping. First adds
|
51
|
+
value only when it is not nil, second adds everything what was defined. Default
|
52
|
+
is `strict`.
|
53
|
+
|
54
|
+
```ruby
|
55
|
+
Spine::Mappings.find(:product, :timestamps).map(product, strategy: :compact)
|
56
|
+
```
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Spine
|
2
|
+
module Mappings
|
3
|
+
autoload :Repository, 'spine/mappings/repository'
|
4
|
+
autoload :Mapper, 'spine/mappings/mapper'
|
5
|
+
autoload :Strategies, 'spine/mappings/strategies'
|
6
|
+
autoload :Mapping, 'spine/mappings/mapping'
|
7
|
+
|
8
|
+
module Syntax
|
9
|
+
autoload :Any, 'spine/mappings/syntax/any'
|
10
|
+
autoload :String, 'spine/mappings/syntax/string'
|
11
|
+
autoload :Integer, 'spine/mappings/syntax/integer'
|
12
|
+
autoload :Decimal, 'spine/mappings/syntax/decimal'
|
13
|
+
autoload :Boolean, 'spine/mappings/syntax/boolean'
|
14
|
+
autoload :Date, 'spine/mappings/syntax/date'
|
15
|
+
autoload :DateTime, 'spine/mappings/syntax/datetime'
|
16
|
+
autoload :Timestamp, 'spine/mappings/syntax/timestamp'
|
17
|
+
autoload :Nested, 'spine/mappings/syntax/nested'
|
18
|
+
autoload :Collection, 'spine/mappings/syntax/collection'
|
19
|
+
autoload :RegisteredMapper, 'spine/mappings/syntax/registered_mapper'
|
20
|
+
end
|
21
|
+
|
22
|
+
module Commands
|
23
|
+
autoload :ActionFetch, 'spine/mappings/commands/action_fetch'
|
24
|
+
autoload :Fetch, 'spine/mappings/commands/fetch'
|
25
|
+
autoload :Intercept, 'spine/mappings/commands/intercept'
|
26
|
+
end
|
27
|
+
|
28
|
+
extend Repository
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Spine
|
2
|
+
module Mappings
|
3
|
+
module Commands
|
4
|
+
class Fetch
|
5
|
+
attr_reader :method_name
|
6
|
+
|
7
|
+
def initialize(method_name)
|
8
|
+
@method_name = method_name
|
9
|
+
end
|
10
|
+
|
11
|
+
def execute(source)
|
12
|
+
if source.respond_to?(method_name)
|
13
|
+
source.public_send(method_name)
|
14
|
+
elsif source.respond_to?('[]')
|
15
|
+
source[method_name]
|
16
|
+
else
|
17
|
+
raise "Source object don't have #{ method_name } method defined."
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Spine
|
2
|
+
module Mappings
|
3
|
+
module Commands
|
4
|
+
class Intercept
|
5
|
+
attr_reader :command, :action, :options
|
6
|
+
|
7
|
+
def initialize(command, action, options = {})
|
8
|
+
@command = command
|
9
|
+
@options = options
|
10
|
+
@action = action
|
11
|
+
end
|
12
|
+
|
13
|
+
def execute(source)
|
14
|
+
value = command.execute(source) || options[:default]
|
15
|
+
if value || !options.fetch(:nullable, false)
|
16
|
+
action.call(value)
|
17
|
+
else
|
18
|
+
value
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Spine
|
2
|
+
module Mappings
|
3
|
+
class Mapper
|
4
|
+
attr_reader :mappings
|
5
|
+
|
6
|
+
def initialize(mappings)
|
7
|
+
@mappings = mappings
|
8
|
+
end
|
9
|
+
|
10
|
+
def map(source, options = {})
|
11
|
+
return nil unless source
|
12
|
+
|
13
|
+
strategy = Strategies.find(options[:strategy])
|
14
|
+
mappings.reduce({}) do |mapped, mapping|
|
15
|
+
strategy.run(mapping.commands, source, mapped)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def map_all(sources, options = {})
|
20
|
+
return nil unless sources || sources.empty?
|
21
|
+
|
22
|
+
sources.map { |source| map(source, options) }
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Spine
|
2
|
+
module Mappings
|
3
|
+
class Mapping
|
4
|
+
include Syntax::Any
|
5
|
+
include Syntax::String
|
6
|
+
include Syntax::Integer
|
7
|
+
include Syntax::Decimal
|
8
|
+
include Syntax::Boolean
|
9
|
+
include Syntax::Date
|
10
|
+
include Syntax::DateTime
|
11
|
+
include Syntax::Timestamp
|
12
|
+
include Syntax::Nested
|
13
|
+
include Syntax::Collection
|
14
|
+
include Syntax::RegisteredMapper
|
15
|
+
|
16
|
+
attr_reader :commands, :repository
|
17
|
+
|
18
|
+
def initialize(repository)
|
19
|
+
@repository = repository
|
20
|
+
@commands = {}
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def register(destination, command)
|
26
|
+
@commands[destination] = command
|
27
|
+
end
|
28
|
+
|
29
|
+
def intercept(command, options, &block)
|
30
|
+
Commands::Intercept.new(command, block, options)
|
31
|
+
end
|
32
|
+
|
33
|
+
def build(destination, options, &block)
|
34
|
+
return Commands::ActionFetch.new(block) if block_given?
|
35
|
+
|
36
|
+
Commands::Fetch.new(options.fetch(:from, destination))
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
module Spine
|
2
|
+
module Mappings
|
3
|
+
# Maps objects to hash.
|
4
|
+
#
|
5
|
+
# == Types
|
6
|
+
# * any - Value as is.
|
7
|
+
# * integer - Integer, default value is 0.
|
8
|
+
# * string - String, default value is ''.
|
9
|
+
# * decimal - Floating point number, default value is 0.0.
|
10
|
+
# * date - Date in ISO 8601 format (YYYY-MM-DD).
|
11
|
+
# * datetime - Date and time in ISO 8601 format (YYYY-MM-DDTHH:MM:SSZ).
|
12
|
+
# * timestamp - Date and time with milliseconds in ISO 8601 format
|
13
|
+
# (YYYY-MM-DDTHH:MM:SS.000Z).
|
14
|
+
# * boolean - Boolean.
|
15
|
+
#
|
16
|
+
# Also you can use other serializers by name. They must be registered
|
17
|
+
# before usage.
|
18
|
+
#
|
19
|
+
# == Options
|
20
|
+
# * from - Method name for source object. Also you can use closure to fetch
|
21
|
+
# value
|
22
|
+
# * nullable - Allows value to be nil.
|
23
|
+
# * default - Default value.
|
24
|
+
#
|
25
|
+
# == Examples
|
26
|
+
#
|
27
|
+
# Serialization.define :tag do
|
28
|
+
# integer :id, from: identity
|
29
|
+
# string :name
|
30
|
+
# end
|
31
|
+
#
|
32
|
+
# Serialization.define :product do
|
33
|
+
# integer :id, from: identity
|
34
|
+
# string :name
|
35
|
+
# decimal :price
|
36
|
+
# boolean :is_available, { |source| source.available? }
|
37
|
+
# date :available_from
|
38
|
+
# date :avaialable_until
|
39
|
+
# tag :tags, nullable: true
|
40
|
+
# timestamp :created_at
|
41
|
+
# timestamp :updated_at
|
42
|
+
# end
|
43
|
+
#
|
44
|
+
# serialized_product = Serialization.serialize(:product, product)
|
45
|
+
# serialized_products = Serialization.serialize_all(:product, products)
|
46
|
+
module Repository
|
47
|
+
def define(name, &block)
|
48
|
+
mappings[name] = parse(block)
|
49
|
+
end
|
50
|
+
|
51
|
+
def find(*names)
|
52
|
+
found_mappings = names.map { |name| mappings.fetch(name) }
|
53
|
+
|
54
|
+
if found_mappings.empty?
|
55
|
+
nil
|
56
|
+
else
|
57
|
+
Mapper.new(found_mappings)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def mappings
|
62
|
+
@mappings ||= {}
|
63
|
+
end
|
64
|
+
|
65
|
+
def parse(dsl)
|
66
|
+
mapping = Mapping.new(self)
|
67
|
+
mapping.instance_eval(&dsl)
|
68
|
+
mapping
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Spine
|
2
|
+
module Mappings
|
3
|
+
module Strategies
|
4
|
+
extend self
|
5
|
+
|
6
|
+
autoload :Strict, 'spine/mappings/strategies/strict'
|
7
|
+
autoload :Compact, 'spine/mappings/strategies/compact'
|
8
|
+
|
9
|
+
def collection
|
10
|
+
@collection ||= default_collection
|
11
|
+
end
|
12
|
+
|
13
|
+
def find(identifier)
|
14
|
+
collection[identifier] || collection[default_identifier]
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def default_identifier
|
20
|
+
:strict
|
21
|
+
end
|
22
|
+
|
23
|
+
def default_collection
|
24
|
+
{ strict: Strict, compact: Compact }
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Spine
|
2
|
+
module Mappings
|
3
|
+
module Strategies
|
4
|
+
module Compact
|
5
|
+
extend self
|
6
|
+
|
7
|
+
def run(commands, source, destination = {})
|
8
|
+
commands.reduce(destination) do |result, (key, command)|
|
9
|
+
value = command.execute(source)
|
10
|
+
result[key] = value if value
|
11
|
+
result
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Spine
|
2
|
+
module Mappings
|
3
|
+
module Strategies
|
4
|
+
module Strict
|
5
|
+
extend self
|
6
|
+
|
7
|
+
def run(commands, source, destination = {})
|
8
|
+
commands.reduce(destination) do |result, (key, command)|
|
9
|
+
result[key] = command.execute(source)
|
10
|
+
result
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Spine
|
2
|
+
module Mappings
|
3
|
+
module Syntax
|
4
|
+
module Boolean
|
5
|
+
def boolean(destination, options = {}, &block)
|
6
|
+
command = build(destination, options, &block)
|
7
|
+
register(
|
8
|
+
destination,
|
9
|
+
intercept(command, options) { |value| !!value }
|
10
|
+
)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Spine
|
2
|
+
module Mappings
|
3
|
+
module Syntax
|
4
|
+
module Collection
|
5
|
+
def collection(type, destination, options = {}, &block)
|
6
|
+
command = build(destination, options, &block)
|
7
|
+
register(
|
8
|
+
destination,
|
9
|
+
intercept(command, options) {
|
10
|
+
|value| repository.find(type).map_all(value)
|
11
|
+
}
|
12
|
+
)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Spine
|
2
|
+
module Mappings
|
3
|
+
module Syntax
|
4
|
+
module Date
|
5
|
+
def date(destination, options = {}, &block)
|
6
|
+
command = build(destination, options, &block)
|
7
|
+
register(
|
8
|
+
destination,
|
9
|
+
intercept(command, options) {
|
10
|
+
|value| value.to_date.iso8601
|
11
|
+
}
|
12
|
+
)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Spine
|
2
|
+
module Mappings
|
3
|
+
module Syntax
|
4
|
+
module DateTime
|
5
|
+
def datetime(destination, options = {}, &block)
|
6
|
+
command = build(destination, options, &block)
|
7
|
+
register(
|
8
|
+
destination,
|
9
|
+
intercept(command, options) { |value|
|
10
|
+
value.to_datetime.iso8601
|
11
|
+
}
|
12
|
+
)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Spine
|
2
|
+
module Mappings
|
3
|
+
module Syntax
|
4
|
+
module Decimal
|
5
|
+
def decimal(destination, options = {}, &block)
|
6
|
+
command = build(destination, options, &block)
|
7
|
+
register(
|
8
|
+
destination,
|
9
|
+
intercept(command, options) { |value| value.to_f }
|
10
|
+
)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Spine
|
2
|
+
module Mappings
|
3
|
+
module Syntax
|
4
|
+
module Integer
|
5
|
+
def integer(destination, options = {}, &block)
|
6
|
+
command = build(destination, options, &block)
|
7
|
+
register(
|
8
|
+
destination,
|
9
|
+
intercept(command, options) { |value| value.to_i }
|
10
|
+
)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Spine
|
2
|
+
module Mappings
|
3
|
+
module Syntax
|
4
|
+
module Nested
|
5
|
+
def nested(destination, options = {}, &block)
|
6
|
+
mapping = repository.parse(block)
|
7
|
+
mapper = Mapper.new([mapping])
|
8
|
+
command = build(destination, options)
|
9
|
+
register(
|
10
|
+
destination,
|
11
|
+
intercept(command, options) { |value| mapper.map(value, options) }
|
12
|
+
)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Spine
|
2
|
+
module Mappings
|
3
|
+
module Syntax
|
4
|
+
module RegisteredMapper
|
5
|
+
def method_missing(name, *arguments, &block)
|
6
|
+
mapper = repository.find(name.to_sym)
|
7
|
+
super and return unless mapper
|
8
|
+
|
9
|
+
destination = arguments.shift
|
10
|
+
options = arguments.shift || {}
|
11
|
+
command = build(destination, options, &block)
|
12
|
+
register(
|
13
|
+
destination,
|
14
|
+
intercept(command, options) { |value| mapper.map(value) }
|
15
|
+
)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Spine
|
2
|
+
module Mappings
|
3
|
+
module Syntax
|
4
|
+
module String
|
5
|
+
def string(destination, options = {}, &block)
|
6
|
+
command = build(destination, options, &block)
|
7
|
+
register(
|
8
|
+
destination,
|
9
|
+
intercept(command, options) { |value| value.to_s }
|
10
|
+
)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Spine
|
2
|
+
module Mappings
|
3
|
+
module Syntax
|
4
|
+
module Timestamp
|
5
|
+
def timestamp(destination, options = {}, &block)
|
6
|
+
command = build(destination, options, &block)
|
7
|
+
register(
|
8
|
+
destination,
|
9
|
+
intercept(command, options) { |value|
|
10
|
+
value.to_datetime.iso8601(3)
|
11
|
+
}
|
12
|
+
)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
RSpec.configure do |config|
|
2
|
+
config.filter_run :focus
|
3
|
+
config.run_all_when_everything_filtered = true
|
4
|
+
|
5
|
+
if config.files_to_run.one?
|
6
|
+
config.default_formatter = 'doc'
|
7
|
+
end
|
8
|
+
|
9
|
+
config.order = :random
|
10
|
+
|
11
|
+
Kernel.srand config.seed
|
12
|
+
|
13
|
+
config.expect_with :rspec do |expectations|
|
14
|
+
expectations.syntax = :expect
|
15
|
+
end
|
16
|
+
|
17
|
+
config.mock_with :rspec do |mocks|
|
18
|
+
mocks.syntax = :expect
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,263 @@
|
|
1
|
+
require 'ostruct'
|
2
|
+
require 'date'
|
3
|
+
require 'spine/mappings'
|
4
|
+
|
5
|
+
module Spine
|
6
|
+
module Mappings
|
7
|
+
describe Mappings do
|
8
|
+
subject { Mappings }
|
9
|
+
|
10
|
+
it 'defines empty mapper' do
|
11
|
+
subject.define(:test) {}
|
12
|
+
expect(subject.find(:test)).to be
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'maps data by coping attribute with same name' do
|
16
|
+
subject.define :test do
|
17
|
+
any :id
|
18
|
+
end
|
19
|
+
|
20
|
+
data = OpenStruct.new(id: 1)
|
21
|
+
mapped = subject.find(:test).map(data)
|
22
|
+
|
23
|
+
expect(mapped[:id]).to eq(1)
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'maps data by coping attribute with different name' do
|
27
|
+
subject.define :test do
|
28
|
+
any :value, from: :test_value
|
29
|
+
end
|
30
|
+
|
31
|
+
data = OpenStruct.new(test_value: 1)
|
32
|
+
mapped = subject.find(:test).map(data)
|
33
|
+
|
34
|
+
expect(mapped[:value]).to eq(1)
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'maps data by coping attribute from closure' do
|
38
|
+
subject.define :test do
|
39
|
+
any(:value) { |source| source.test_value }
|
40
|
+
end
|
41
|
+
|
42
|
+
data = OpenStruct.new(test_value: 1)
|
43
|
+
mapped = subject.find(:test).map(data)
|
44
|
+
|
45
|
+
expect(mapped[:value]).to eq(1)
|
46
|
+
end
|
47
|
+
|
48
|
+
it 'maps empty data as nil' do
|
49
|
+
subject.define(:test) do
|
50
|
+
any :value
|
51
|
+
end
|
52
|
+
|
53
|
+
data = OpenStruct.new(value: nil)
|
54
|
+
mapped = subject.find(:test).map(data)
|
55
|
+
|
56
|
+
expect(mapped[:value]).to eq(nil)
|
57
|
+
end
|
58
|
+
|
59
|
+
it 'maps data with empty mapper' do
|
60
|
+
subject.define(:test) {}
|
61
|
+
expect(subject.find(:test).map(Object.new)).to eq({})
|
62
|
+
end
|
63
|
+
|
64
|
+
it 'maps hash to hash' do
|
65
|
+
subject.define(:test) do
|
66
|
+
any :x, from: :y
|
67
|
+
end
|
68
|
+
|
69
|
+
expect(subject.find(:test).map(y: 1)).to eq(x: 1)
|
70
|
+
end
|
71
|
+
|
72
|
+
describe 'when using interceptors' do
|
73
|
+
it 'maps data by converting it to string' do
|
74
|
+
subject.define :test do
|
75
|
+
string :value
|
76
|
+
end
|
77
|
+
|
78
|
+
data = OpenStruct.new(value: 1)
|
79
|
+
mapped = subject.find(:test).map(data)
|
80
|
+
|
81
|
+
expect(mapped[:value]).to eq('1')
|
82
|
+
end
|
83
|
+
|
84
|
+
it 'maps data by converting it to integer' do
|
85
|
+
subject.define :test do
|
86
|
+
integer :value
|
87
|
+
end
|
88
|
+
|
89
|
+
data = OpenStruct.new(value: "1")
|
90
|
+
mapped = subject.find(:test).map(data)
|
91
|
+
|
92
|
+
expect(mapped[:value]).to eq(1)
|
93
|
+
end
|
94
|
+
|
95
|
+
it 'maps nil data when nullable' do
|
96
|
+
subject.define :test do
|
97
|
+
integer :value, nullable: true
|
98
|
+
end
|
99
|
+
|
100
|
+
data = OpenStruct.new(value: nil)
|
101
|
+
mapped = subject.find(:test).map(data)
|
102
|
+
|
103
|
+
expect(mapped[:value]).to eq(nil)
|
104
|
+
end
|
105
|
+
|
106
|
+
it 'maps nil data to default' do
|
107
|
+
subject.define :test do
|
108
|
+
integer :value, default: 1
|
109
|
+
end
|
110
|
+
|
111
|
+
data = OpenStruct.new(value: nil)
|
112
|
+
mapped = subject.find(:test).map(data)
|
113
|
+
|
114
|
+
expect(mapped[:value]).to eq(1)
|
115
|
+
end
|
116
|
+
|
117
|
+
it 'maps data by converting it to decimal' do
|
118
|
+
subject.define :test do
|
119
|
+
decimal :value
|
120
|
+
end
|
121
|
+
|
122
|
+
data = OpenStruct.new(value: "1.5")
|
123
|
+
mapped = subject.find(:test).map(data)
|
124
|
+
|
125
|
+
expect(mapped[:value]).to eq(1.5)
|
126
|
+
end
|
127
|
+
|
128
|
+
it 'maps data by converting it to boolean' do
|
129
|
+
subject.define :test do
|
130
|
+
boolean :value
|
131
|
+
end
|
132
|
+
|
133
|
+
data = OpenStruct.new(value: '1')
|
134
|
+
mapped = subject.find(:test).map(data)
|
135
|
+
|
136
|
+
expect(mapped[:value]).to eq(true)
|
137
|
+
end
|
138
|
+
|
139
|
+
it 'maps data by converting it to date' do
|
140
|
+
subject.define :test do
|
141
|
+
date :value
|
142
|
+
end
|
143
|
+
|
144
|
+
value = Time.now
|
145
|
+
data = OpenStruct.new(value: value)
|
146
|
+
mapped = subject.find(:test).map(data)
|
147
|
+
|
148
|
+
expect(mapped[:value]).to eq(value.to_date.iso8601)
|
149
|
+
end
|
150
|
+
|
151
|
+
it 'maps data by converting it to date time' do
|
152
|
+
subject.define :test do
|
153
|
+
datetime :value
|
154
|
+
end
|
155
|
+
|
156
|
+
value = Time.now
|
157
|
+
data = OpenStruct.new(value: value)
|
158
|
+
mapped = subject.find(:test).map(data)
|
159
|
+
|
160
|
+
expect(mapped[:value]).to eq(value.to_datetime.iso8601)
|
161
|
+
end
|
162
|
+
|
163
|
+
it 'maps data by converting it to timestamp' do
|
164
|
+
subject.define :test do
|
165
|
+
timestamp :value
|
166
|
+
end
|
167
|
+
|
168
|
+
value = Time.now
|
169
|
+
data = OpenStruct.new(value: value)
|
170
|
+
mapped = subject.find(:test).map(data)
|
171
|
+
|
172
|
+
expect(mapped[:value]).to eq(value.to_datetime.iso8601(3))
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
describe 'when using other mappings' do
|
177
|
+
it 'maps data by converting it by other mapper' do
|
178
|
+
subject.define :nested_object do
|
179
|
+
any :nested_value
|
180
|
+
end
|
181
|
+
subject.define(:test) do
|
182
|
+
nested_object :value
|
183
|
+
end
|
184
|
+
|
185
|
+
data = double(value: double(nested_value: 1))
|
186
|
+
mapped = subject.find(:test).map(data)
|
187
|
+
|
188
|
+
expect(mapped[:value][:nested_value])
|
189
|
+
.to eq(data.value.nested_value)
|
190
|
+
end
|
191
|
+
|
192
|
+
it 'maps data by converting it by other complex mapper' do
|
193
|
+
subject.define :nested_object do
|
194
|
+
any :nested_value, from: :test_value
|
195
|
+
end
|
196
|
+
subject.define :test do
|
197
|
+
nested_object :value
|
198
|
+
end
|
199
|
+
|
200
|
+
data = double(value: double(test_value: 1))
|
201
|
+
mapped = subject.find(:test).map(data)
|
202
|
+
|
203
|
+
expect(mapped[:value][:nested_value])
|
204
|
+
.to eq(data.value.test_value)
|
205
|
+
end
|
206
|
+
|
207
|
+
it 'raises error when nested mapper is not defined' do
|
208
|
+
expect {
|
209
|
+
subject.define :test do
|
210
|
+
not_defined_nested_object :value
|
211
|
+
end
|
212
|
+
}.to raise_error(KeyError)
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
describe 'when using mapping strategies' do
|
217
|
+
it 'maps using strict strategy' do
|
218
|
+
subject.define(:test) do
|
219
|
+
any :x
|
220
|
+
end
|
221
|
+
|
222
|
+
expect(subject.find(:test).map(y: 1).keys).to include(:x)
|
223
|
+
end
|
224
|
+
|
225
|
+
it 'maps using compact strategy' do
|
226
|
+
subject.define(:test) do
|
227
|
+
any :x
|
228
|
+
end
|
229
|
+
|
230
|
+
source = { y: 1 }
|
231
|
+
mapped = subject.find(:test).map(source, strategy: :compact)
|
232
|
+
expect(mapped.keys).not_to include(:x)
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
describe 'when mapping nested objects' do
|
237
|
+
it 'maps nested hash' do
|
238
|
+
subject.define(:test) do
|
239
|
+
nested :x do
|
240
|
+
any :y
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
source = OpenStruct.new(x: OpenStruct.new(y: 1))
|
245
|
+
mapped = subject.find(:test).map(source)
|
246
|
+
expect(mapped[:x][:y]).to eq(source.x.y)
|
247
|
+
end
|
248
|
+
|
249
|
+
it 'maps nested object' do
|
250
|
+
subject.define(:test) do
|
251
|
+
nested :x do
|
252
|
+
any :y
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
source = { x: { y: 1 } }
|
257
|
+
mapped = subject.find(:test).map(source)
|
258
|
+
expect(mapped[:x][:y]).to eq(source[:x][:y])
|
259
|
+
end
|
260
|
+
end
|
261
|
+
end
|
262
|
+
end
|
263
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
lib = File.expand_path('../lib', __FILE__)
|
2
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
|
+
require 'spine/mappings/version'
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = "spine-mappings"
|
7
|
+
spec.version = Spine::Mappings::VERSION
|
8
|
+
spec.authors = ["TOGGL LLC"]
|
9
|
+
spec.email = ["support@toggl.com"]
|
10
|
+
spec.summary = ''
|
11
|
+
spec.description = ''
|
12
|
+
spec.homepage = 'https://github.com/rspine/mappings'
|
13
|
+
spec.license = 'BSD-3-Clause'
|
14
|
+
|
15
|
+
spec.files = `git ls-files -z`.split("\x0")
|
16
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
17
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
18
|
+
spec.require_paths = ["lib"]
|
19
|
+
|
20
|
+
spec.add_development_dependency 'bundler', "~> 1.7"
|
21
|
+
spec.add_development_dependency 'rake', "~> 10.0"
|
22
|
+
spec.add_development_dependency 'rspec', "~> 3.2"
|
23
|
+
spec.add_development_dependency 'codeclimate-test-reporter', "~> 0.4"
|
24
|
+
end
|
metadata
ADDED
@@ -0,0 +1,137 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: spine-mappings
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- TOGGL LLC
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-05-24 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.2'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '3.2'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: codeclimate-test-reporter
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0.4'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0.4'
|
69
|
+
description: ''
|
70
|
+
email:
|
71
|
+
- support@toggl.com
|
72
|
+
executables: []
|
73
|
+
extensions: []
|
74
|
+
extra_rdoc_files: []
|
75
|
+
files:
|
76
|
+
- ".gitignore"
|
77
|
+
- ".rspec"
|
78
|
+
- CHANGELOG.md
|
79
|
+
- Gemfile
|
80
|
+
- Gemfile.lock
|
81
|
+
- LICENSE
|
82
|
+
- README.md
|
83
|
+
- Rakefile
|
84
|
+
- lib/spine/mappings.rb
|
85
|
+
- lib/spine/mappings/commands/action_fetch.rb
|
86
|
+
- lib/spine/mappings/commands/fetch.rb
|
87
|
+
- lib/spine/mappings/commands/intercept.rb
|
88
|
+
- lib/spine/mappings/mapper.rb
|
89
|
+
- lib/spine/mappings/mapping.rb
|
90
|
+
- lib/spine/mappings/repository.rb
|
91
|
+
- lib/spine/mappings/strategies.rb
|
92
|
+
- lib/spine/mappings/strategies/compact.rb
|
93
|
+
- lib/spine/mappings/strategies/strict.rb
|
94
|
+
- lib/spine/mappings/syntax/any.rb
|
95
|
+
- lib/spine/mappings/syntax/boolean.rb
|
96
|
+
- lib/spine/mappings/syntax/collection.rb
|
97
|
+
- lib/spine/mappings/syntax/date.rb
|
98
|
+
- lib/spine/mappings/syntax/datetime.rb
|
99
|
+
- lib/spine/mappings/syntax/decimal.rb
|
100
|
+
- lib/spine/mappings/syntax/integer.rb
|
101
|
+
- lib/spine/mappings/syntax/nested.rb
|
102
|
+
- lib/spine/mappings/syntax/registered_mapper.rb
|
103
|
+
- lib/spine/mappings/syntax/string.rb
|
104
|
+
- lib/spine/mappings/syntax/timestamp.rb
|
105
|
+
- lib/spine/mappings/version.rb
|
106
|
+
- spec/config/coverage.rb
|
107
|
+
- spec/config/default.rb
|
108
|
+
- spec/spine/mappings/mappings_spec.rb
|
109
|
+
- spine-mappings.gemspec
|
110
|
+
homepage: https://github.com/rspine/mappings
|
111
|
+
licenses:
|
112
|
+
- BSD-3-Clause
|
113
|
+
metadata: {}
|
114
|
+
post_install_message:
|
115
|
+
rdoc_options: []
|
116
|
+
require_paths:
|
117
|
+
- lib
|
118
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
119
|
+
requirements:
|
120
|
+
- - ">="
|
121
|
+
- !ruby/object:Gem::Version
|
122
|
+
version: '0'
|
123
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
124
|
+
requirements:
|
125
|
+
- - ">="
|
126
|
+
- !ruby/object:Gem::Version
|
127
|
+
version: '0'
|
128
|
+
requirements: []
|
129
|
+
rubyforge_project:
|
130
|
+
rubygems_version: 2.4.5
|
131
|
+
signing_key:
|
132
|
+
specification_version: 4
|
133
|
+
summary: ''
|
134
|
+
test_files:
|
135
|
+
- spec/config/coverage.rb
|
136
|
+
- spec/config/default.rb
|
137
|
+
- spec/spine/mappings/mappings_spec.rb
|