model-mapper 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +23 -0
- data/README.md +227 -4
- data/lib/mappable/class_methods.rb +51 -0
- data/lib/mappable/compiler.rb +128 -0
- data/lib/mappable/mapping/class_methods.rb +124 -0
- data/lib/mappable/mapping.rb +84 -50
- data/lib/mappable/utils.rb +11 -2
- data/lib/mappable/version.rb +6 -0
- data/lib/mappable.rb +7 -22
- metadata +26 -24
- data/.gitignore +0 -51
- data/.rubocop.yml +0 -12
- data/.ruby-gemset +0 -1
- data/.ruby-version +0 -1
- data/.travis.yml +0 -17
- data/Gemfile +0 -15
- data/Gemfile.lock +0 -54
- data/gemfiles/travis.gemfile +0 -1
- data/model-mapper.gemspec +0 -15
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 775e838aa683bcb13a626497d487253ea1e23c27eef15df92a92d99ffdad19f8
|
|
4
|
+
data.tar.gz: 39b2177d2a088282f16135540f0cbfd9fef501699981bb358981e96b0f6ebdb8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0ec1989c92c08271125531e1809a845bae74d85b8d23276fd2ac084ed20db8ebcd80966efd44bc82c6264fbb326a92c26ca68abfa82b851fa73e3de0da54d784
|
|
7
|
+
data.tar.gz: 7179f959eccb52dee9d34ca3c8be8572b19f49b97b08c05e4366a889a8dd720a17b3d557fa93989b7610bb43e956eee28e8ef49d35cb607360281a413ef8d072
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.2.0](https://github.com/dougyouch/mappable/compare/v0.1.0...v0.2.0) (2026-10-05)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### ⚠ BREAKING CHANGES
|
|
7
|
+
|
|
8
|
+
* **mapping:** the internal Mapping instance methods map_data, skip?, get_value, call_method and call_map_method are removed.
|
|
9
|
+
|
|
10
|
+
### Features
|
|
11
|
+
|
|
12
|
+
* **mappable:** add map_from_<name> to copy data back from the destination ([2b1503f](https://github.com/dougyouch/mappable/commit/2b1503f5a619b77f1d2e342ecf06d5ff429c1392))
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
### Bug Fixes
|
|
16
|
+
|
|
17
|
+
* **mappable:** support map_to on anonymous classes and stop modifying options ([a5b3b7f](https://github.com/dougyouch/mappable/commit/a5b3b7f1ea0b2477f2213cd3e9c28fcdecda0934))
|
|
18
|
+
* **utils:** keep capital letters when building mapping class names ([5160516](https://github.com/dougyouch/mappable/commit/5160516a1d10bd9cf8593a34496bf95ee13a3330))
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
### Performance Improvements
|
|
22
|
+
|
|
23
|
+
* **mapping:** compile mappings into plain ruby methods ([8854abb](https://github.com/dougyouch/mappable/commit/8854abb763f976bbc76a4bd7cc00e9c1dde50908))
|
data/README.md
CHANGED
|
@@ -1,5 +1,228 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Mappable
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
[](https://github.com/dougyouch/mappable/actions/workflows/ci.yml)
|
|
6
|
+
[](https://github.com/dougyouch/mappable/actions/workflows/ci.yml)
|
|
7
|
+
[](https://github.com/dougyouch/mappable/actions/workflows/ci.yml)
|
|
8
|
+
[](https://rubygems.org/gems/model-mapper)
|
|
9
|
+
|
|
10
|
+
[API reference](https://rubydoc.info/gems/model-mapper) · [Changelog](CHANGELOG.md) · [Architecture](ARCHITECTURE.md)
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
Requires Ruby 3.2 or newer. The gem is published as `model-mapper`:
|
|
15
|
+
|
|
16
|
+
```ruby
|
|
17
|
+
gem 'model-mapper'
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
```ruby
|
|
21
|
+
require 'model-mapper' # Bundler does this for you
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Quick Start
|
|
25
|
+
|
|
26
|
+
Map a `User` to a `Contact`, combining `first_name` and `last_name` into `name`, and split `name` back out on the way back:
|
|
27
|
+
|
|
28
|
+
```ruby
|
|
29
|
+
User = Struct.new(:first_name, :last_name, :email)
|
|
30
|
+
Contact = Struct.new(:name, :email_address)
|
|
31
|
+
|
|
32
|
+
class User
|
|
33
|
+
include Mappable
|
|
34
|
+
|
|
35
|
+
map_to(:contact) do
|
|
36
|
+
# user -> contact
|
|
37
|
+
custom_map(:name) { |user| "#{user.first_name} #{user.last_name}" }
|
|
38
|
+
|
|
39
|
+
# contact -> user
|
|
40
|
+
custom_map_back(:first_name) { |contact| contact.name.split(' ', 2).first }
|
|
41
|
+
custom_map_back(:last_name) { |contact| contact.name.split(' ', 2).last }
|
|
42
|
+
|
|
43
|
+
# both directions: email -> email_address and email_address -> email
|
|
44
|
+
map :email, :email_address
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
user = User.new('Ada', 'Lovelace', 'ada@example.com')
|
|
49
|
+
|
|
50
|
+
contact = user.map_to_contact(Contact.new)
|
|
51
|
+
contact.name # => "Ada Lovelace"
|
|
52
|
+
contact.email_address # => "ada@example.com"
|
|
53
|
+
|
|
54
|
+
copy = User.new.map_from_contact(contact)
|
|
55
|
+
copy.first_name # => "Ada"
|
|
56
|
+
copy.last_name # => "Lovelace"
|
|
57
|
+
copy.email # => "ada@example.com"
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
`map_to(:contact)` creates a mapping class, `User::ContactMapping`, and two instance methods:
|
|
61
|
+
|
|
62
|
+
| Method | Does | Returns |
|
|
63
|
+
|---|---|---|
|
|
64
|
+
| `map_to_contact(dest)` | copies the user's fields to `dest` | `dest` |
|
|
65
|
+
| `map_from_contact(src)` | copies the fields of `src` back to the user | the user |
|
|
66
|
+
|
|
67
|
+
Any objects work as long as they have getters for the fields being read and setters (`name=`) for the fields being written: Structs, plain Ruby classes, ActiveRecord or ActiveModel models.
|
|
68
|
+
|
|
69
|
+
## Declaring Mappings
|
|
70
|
+
|
|
71
|
+
### `map`: copy a field, both ways
|
|
72
|
+
|
|
73
|
+
```ruby
|
|
74
|
+
map :email # email -> email, and back
|
|
75
|
+
map :email, :email_address # email -> email_address, and email_address -> email
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### `custom_map`: compute a field
|
|
79
|
+
|
|
80
|
+
`custom_map` sets a destination field from a block, or from a method on the mapping class. It's one-way; declare the reverse with `custom_map_back`.
|
|
81
|
+
|
|
82
|
+
```ruby
|
|
83
|
+
map_to(:contact) do
|
|
84
|
+
custom_map(:name) { |user| "#{user.first_name} #{user.last_name}" }
|
|
85
|
+
|
|
86
|
+
custom_map :initials # calls initials(user)
|
|
87
|
+
custom_map :display_name, :full_name # calls full_name(user)
|
|
88
|
+
|
|
89
|
+
def initials(user)
|
|
90
|
+
"#{user.first_name[0]}#{user.last_name[0]}"
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
private
|
|
94
|
+
|
|
95
|
+
def full_name(user) # custom methods can be private
|
|
96
|
+
[user.first_name, user.last_name].compact.join(' ')
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### `custom_map_back`: compute a field on the way back
|
|
102
|
+
|
|
103
|
+
`custom_map_back` sets a field on the source object from the destination object. It takes the same arguments as `custom_map`.
|
|
104
|
+
|
|
105
|
+
```ruby
|
|
106
|
+
custom_map_back(:first_name) { |contact| contact.name.split(' ', 2).first }
|
|
107
|
+
custom_map_back :last_name # calls last_name(contact)
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Conditions
|
|
111
|
+
|
|
112
|
+
Skip a field unless a condition holds. Every mapping method takes these options:
|
|
113
|
+
|
|
114
|
+
| Option | Checked on |
|
|
115
|
+
|---|---|
|
|
116
|
+
| `if:` / `unless:` | the mapping instance |
|
|
117
|
+
| `if_src:` / `unless_src:` | the object being read from |
|
|
118
|
+
| `if_dest:` / `unless_dest:` | the object being written to |
|
|
119
|
+
|
|
120
|
+
A condition is a method name, called on that object, or a proc, run with that object as `self` and passed as its argument. A lambda that takes no arguments works too.
|
|
121
|
+
|
|
122
|
+
```ruby
|
|
123
|
+
map_to(:contact) do
|
|
124
|
+
map :email, :email_address, if_src: :email_verified?
|
|
125
|
+
map :phone, unless_dest: :phone_locked?
|
|
126
|
+
map :notes, if: -> { include_notes }
|
|
127
|
+
map :status, unless_src: ->(user) { user.status.nil? }
|
|
128
|
+
|
|
129
|
+
attr_accessor :include_notes
|
|
130
|
+
end
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Each `map` call also declares the reverse mapping, and the reverse swaps `_src` and `_dest` conditions so they still check the same object. In the example above, `if_src: :email_verified?` checks the user in both directions: on the way to the contact the user is the source, and on the way back it's the destination.
|
|
134
|
+
|
|
135
|
+
When a field has several conditions, all of them must pass.
|
|
136
|
+
|
|
137
|
+
## Using Mapping Classes Directly
|
|
138
|
+
|
|
139
|
+
The mapping class is a regular class you can instantiate, which is how you pass it state such as the `include_notes` flag above:
|
|
140
|
+
|
|
141
|
+
```ruby
|
|
142
|
+
mapping = User::ContactMapping.new
|
|
143
|
+
mapping.include_notes = true
|
|
144
|
+
mapping.map(user, Contact.new) # => the contact
|
|
145
|
+
mapping.map_back(contact, User.new) # => the user
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
You can also define mapping classes without `map_to`:
|
|
149
|
+
|
|
150
|
+
```ruby
|
|
151
|
+
class ContactMapping
|
|
152
|
+
include Mappable::Mapping
|
|
153
|
+
|
|
154
|
+
map :email, :email_address
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
ContactMapping.new.map(user, Contact.new)
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### `map_to` options
|
|
161
|
+
|
|
162
|
+
```ruby
|
|
163
|
+
# name the mapping class (defaults to ContactMapping, set as a constant of the including class)
|
|
164
|
+
map_to(:contact, class_name: 'PersonMapper') { ... }
|
|
165
|
+
|
|
166
|
+
# inherit mappings from another mapping class and add to them
|
|
167
|
+
map_to(:admin_contact, base_class: User::ContactMapping) do
|
|
168
|
+
map :role
|
|
169
|
+
end
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
A subclass gets the mappings its base class had when the subclass declared its first mapping. Declare a base class's mappings before you subclass it.
|
|
173
|
+
|
|
174
|
+
### Inspecting mappings
|
|
175
|
+
|
|
176
|
+
`mappings` and `map_back_mappings` return the options for each field. Any extra options you pass, such as a description, are kept:
|
|
177
|
+
|
|
178
|
+
```ruby
|
|
179
|
+
map :email, :email_address, description: 'primary email'
|
|
180
|
+
|
|
181
|
+
User::ContactMapping.mappings[:email_address]
|
|
182
|
+
# => {src: :email, getter: "email", dest: :email_address, setter: "email_address=", description: "primary email"}
|
|
183
|
+
|
|
184
|
+
User.maps # => {contact: User::ContactMapping}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## Performance
|
|
188
|
+
|
|
189
|
+
Mappable doesn't interpret the mapping hash at runtime. Each `map`, `custom_map` or `custom_map_back` call regenerates the mapping class's `map` and `map_back` methods as straight-line Ruby, so mapping an object is a list of getter and setter calls:
|
|
190
|
+
|
|
191
|
+
```ruby
|
|
192
|
+
map :email, :email_address, if_dest: :persisted?
|
|
193
|
+
custom_map(:name) { |user| "#{user.first_name} #{user.last_name}" }
|
|
194
|
+
|
|
195
|
+
# compiles to
|
|
196
|
+
def map(src_model, dest_model)
|
|
197
|
+
dest_model.email_address = src_model.email if dest_model.persisted?
|
|
198
|
+
dest_model.name = MAPPABLE_PROCS[0].call(src_model)
|
|
199
|
+
dest_model
|
|
200
|
+
end
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Mapping 9 fields between Structs (one computed field, one condition), on Ruby 4.0.7 without YJIT:
|
|
204
|
+
|
|
205
|
+
| | ns per object |
|
|
206
|
+
|---|---|
|
|
207
|
+
| hand-written assignments | 490 |
|
|
208
|
+
| `map_to_contact` (this version) | 636 |
|
|
209
|
+
| `map_to_contact` (model-mapper 0.1.0) | 4560 |
|
|
210
|
+
|
|
211
|
+
Recompiling happens when mappings are declared, so declare them when your classes load, not per request.
|
|
212
|
+
|
|
213
|
+
## Development
|
|
214
|
+
|
|
215
|
+
```bash
|
|
216
|
+
bundle install
|
|
217
|
+
bundle exec rspec # tests, with line and branch coverage in coverage/
|
|
218
|
+
bundle exec rubocop # lint
|
|
219
|
+
bundle exec yard # API docs in doc/
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
CI runs RuboCop, requires every public API to have YARD docs, and runs the specs on Ruby 3.2 and on the Ruby in `.ruby-version`, where it requires 100% line and branch coverage.
|
|
223
|
+
|
|
224
|
+
Releases are automated with [release-please](https://github.com/googleapis/release-please): [conventional commits](https://www.conventionalcommits.org/) on `master` keep a release PR up to date, and merging it tags the release and publishes the gem to RubyGems.
|
|
225
|
+
|
|
226
|
+
## License
|
|
227
|
+
|
|
228
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mappable
|
|
4
|
+
# Class methods added by including {Mappable}
|
|
5
|
+
module ClassMethods
|
|
6
|
+
# @return [Hash{Symbol => Class}] mapping classes by name, as declared with {#map_to}
|
|
7
|
+
def maps
|
|
8
|
+
{}.freeze
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Creates a mapping class, a constant of this class, and two instance methods that use it:
|
|
12
|
+
#
|
|
13
|
+
# - `map_to_<name>(dest)` copies this object's fields to `dest` and returns `dest`
|
|
14
|
+
# - `map_from_<name>(src)` copies the fields of `src` back to this object and returns `self`
|
|
15
|
+
#
|
|
16
|
+
# @example
|
|
17
|
+
# class User
|
|
18
|
+
# include Mappable
|
|
19
|
+
#
|
|
20
|
+
# map_to(:contact) do
|
|
21
|
+
# map :email, :email_address
|
|
22
|
+
# end
|
|
23
|
+
# end
|
|
24
|
+
#
|
|
25
|
+
# user.map_to_contact(Contact.new) # => the contact, with email_address set
|
|
26
|
+
# User.new.map_from_contact(contact) # => the user, with email set
|
|
27
|
+
#
|
|
28
|
+
# @param name [Symbol, String] names the methods and the mapping class (`:contact` creates `ContactMapping`)
|
|
29
|
+
# @param options [Hash] see {Mapping.create}
|
|
30
|
+
# @yield evaluated in the mapping class, to declare its mappings (see {Mapping::ClassMethods})
|
|
31
|
+
# @return [Class] the mapping class
|
|
32
|
+
def map_to(name, options = {}, &)
|
|
33
|
+
mapping = Mapping.create(self, name, options, &)
|
|
34
|
+
add_value_to_class_method(:maps, name.to_sym => mapping)
|
|
35
|
+
# referenced by its constant name, resolved from this class, so anonymous classes work too
|
|
36
|
+
mapping_const = mapping.name.split('::').last
|
|
37
|
+
|
|
38
|
+
class_eval(<<~RUBY, __FILE__, __LINE__ + 1)
|
|
39
|
+
def map_to_#{name}(dest) # def map_to_contact(dest)
|
|
40
|
+
#{mapping_const}.new.map(self, dest) # ContactMapping.new.map(self, dest)
|
|
41
|
+
end # end
|
|
42
|
+
|
|
43
|
+
def map_from_#{name}(src) # def map_from_contact(src)
|
|
44
|
+
#{mapping_const}.new.map_back(src, self) # ContactMapping.new.map_back(src, self)
|
|
45
|
+
end # end
|
|
46
|
+
RUBY
|
|
47
|
+
|
|
48
|
+
mapping
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mappable
|
|
4
|
+
# Turns mapping options into the source of a Ruby method, so running a mapping is a
|
|
5
|
+
# series of plain method calls instead of hash lookups and `public_send`.
|
|
6
|
+
#
|
|
7
|
+
# @example
|
|
8
|
+
# # map :email, :email_address, if_dest: :persisted?
|
|
9
|
+
# # custom_map(:name) { |user| "#{user.first_name} #{user.last_name}" }
|
|
10
|
+
# #
|
|
11
|
+
# # compiles to
|
|
12
|
+
# def map(src_model, dest_model)
|
|
13
|
+
# dest_model.email_address = src_model.email if dest_model.persisted?
|
|
14
|
+
# dest_model.name = MAPPABLE_PROCS[0].call(src_model)
|
|
15
|
+
# dest_model
|
|
16
|
+
# end
|
|
17
|
+
#
|
|
18
|
+
# Procs can't be written into source, so they are collected in {#procs} and the
|
|
19
|
+
# generated code reads them from the {PROCS_CONSTANT} constant on the mapping class.
|
|
20
|
+
# Names that can't be called with dot syntax are called with `public_send`.
|
|
21
|
+
#
|
|
22
|
+
# @api private
|
|
23
|
+
class Compiler
|
|
24
|
+
# Name of the constant on the mapping class that holds {#procs}
|
|
25
|
+
PROCS_CONSTANT = :MAPPABLE_PROCS
|
|
26
|
+
|
|
27
|
+
# A method name that can be called with dot syntax
|
|
28
|
+
METHOD_NAME = /\A[a-zA-Z_]\w*[?!]?\z/
|
|
29
|
+
|
|
30
|
+
# A setter name that can be called with assignment syntax
|
|
31
|
+
SETTER_NAME = /\A[a-zA-Z_]\w*=\z/
|
|
32
|
+
|
|
33
|
+
# Condition options in the order they are checked: option, receiver, negated
|
|
34
|
+
CONDITIONS = [
|
|
35
|
+
[:if, 'self', false],
|
|
36
|
+
[:unless, 'self', true],
|
|
37
|
+
[:if_dest, 'dest_model', false],
|
|
38
|
+
[:unless_dest, 'dest_model', true],
|
|
39
|
+
[:if_src, 'src_model', false],
|
|
40
|
+
[:unless_src, 'src_model', true]
|
|
41
|
+
].freeze
|
|
42
|
+
|
|
43
|
+
# @return [Array<Proc>] the procs referenced by the generated source, by index
|
|
44
|
+
attr_reader :procs
|
|
45
|
+
|
|
46
|
+
def initialize
|
|
47
|
+
@procs = []
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# @param method_name [Symbol] name of the generated method
|
|
51
|
+
# @param mappings [Hash{Symbol => Hash}] mapping options, as built by {Mapping::ClassMethods}
|
|
52
|
+
# @return [String] source of a method that takes (src_model, dest_model) and returns dest_model
|
|
53
|
+
# @raise [ArgumentError] when a condition or map method is not a Symbol, String or Proc
|
|
54
|
+
def method_source(method_name, mappings)
|
|
55
|
+
lines = mappings.each_value.map { |options| " #{assignment(options)}" }
|
|
56
|
+
["def #{method_name}(src_model, dest_model)", *lines, ' dest_model', 'end'].join("\n")
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
def assignment(options)
|
|
62
|
+
line = setter_call(options[:setter].to_s, value(options))
|
|
63
|
+
condition = condition(options)
|
|
64
|
+
condition ? "#{line} if #{condition}" : line
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def value(options)
|
|
68
|
+
return method_call('src_model', options[:getter].to_s) unless options[:map_method]
|
|
69
|
+
|
|
70
|
+
map_method_call(options[:map_method])
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def map_method_call(map_method)
|
|
74
|
+
case map_method
|
|
75
|
+
when Symbol, String
|
|
76
|
+
method_call('self', map_method.to_s, 'src_model')
|
|
77
|
+
when Proc
|
|
78
|
+
"#{proc_ref(map_method)}.call(src_model)"
|
|
79
|
+
else
|
|
80
|
+
raise ArgumentError, "map method must be a Symbol, String or Proc, got #{map_method.inspect}"
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def condition(options)
|
|
85
|
+
checks = CONDITIONS.filter_map do |option, receiver, negated|
|
|
86
|
+
next unless options[option]
|
|
87
|
+
|
|
88
|
+
check = condition_call(receiver, options[option])
|
|
89
|
+
negated ? "!(#{check})" : check
|
|
90
|
+
end
|
|
91
|
+
checks.join(' && ') unless checks.empty?
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Proc conditions run with the receiver as self and get it as their argument;
|
|
95
|
+
# lambdas that take no arguments are called without it
|
|
96
|
+
def condition_call(receiver, condition)
|
|
97
|
+
case condition
|
|
98
|
+
when Symbol, String
|
|
99
|
+
method_call(receiver, condition.to_s)
|
|
100
|
+
when Proc
|
|
101
|
+
arg = condition.lambda? && condition.arity.zero? ? '' : "#{receiver}, "
|
|
102
|
+
"#{receiver}.instance_exec(#{arg}&#{proc_ref(condition)})"
|
|
103
|
+
else
|
|
104
|
+
raise ArgumentError, "condition must be a Symbol, String or Proc, got #{condition.inspect}"
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# self.name(...) can call the mapping's private methods; other receivers only public ones
|
|
109
|
+
def method_call(receiver, name, *args)
|
|
110
|
+
call_args = args.empty? ? '' : "(#{args.join(', ')})"
|
|
111
|
+
return "#{receiver}.#{name}#{call_args}" if METHOD_NAME.match?(name)
|
|
112
|
+
|
|
113
|
+
send_method = receiver == 'self' ? '__send__' : 'public_send'
|
|
114
|
+
"#{receiver}.#{send_method}(#{[name.to_sym.inspect, *args].join(', ')})"
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def setter_call(setter, value)
|
|
118
|
+
return "dest_model.#{setter.delete_suffix('=')} = #{value}" if SETTER_NAME.match?(setter)
|
|
119
|
+
|
|
120
|
+
"dest_model.public_send(#{setter.to_sym.inspect}, #{value})"
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def proc_ref(proc)
|
|
124
|
+
@procs << proc
|
|
125
|
+
"#{PROCS_CONSTANT}[#{@procs.size - 1}]"
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mappable
|
|
4
|
+
module Mapping
|
|
5
|
+
# The mapping DSL, added to classes that include {Mapping}.
|
|
6
|
+
#
|
|
7
|
+
# ## Conditions
|
|
8
|
+
#
|
|
9
|
+
# {#map}, {#custom_map} and {#custom_map_back} take conditions that skip the field
|
|
10
|
+
# when they don't hold:
|
|
11
|
+
#
|
|
12
|
+
# - `:if` / `:unless` - checked on the mapping instance
|
|
13
|
+
# - `:if_src` / `:unless_src` - checked on the object being read from
|
|
14
|
+
# - `:if_dest` / `:unless_dest` - checked on the object being written to
|
|
15
|
+
#
|
|
16
|
+
# A condition is a method name, called on that object, or a proc, run with the
|
|
17
|
+
# object as `self` and passed as its argument (a lambda may take no arguments).
|
|
18
|
+
# For {#map}, the reverse mapping swaps the `_src` and `_dest` conditions so they
|
|
19
|
+
# still check the same object.
|
|
20
|
+
module ClassMethods
|
|
21
|
+
# @return [Hash{Symbol => Hash}] options for each field {Mapping#map} sets, by field name
|
|
22
|
+
def mappings
|
|
23
|
+
{}.freeze
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# @return [Hash{Symbol => Hash}] options for each field {Mapping#map_back} sets, by field name
|
|
27
|
+
def map_back_mappings
|
|
28
|
+
{}.freeze
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Copies the `src` field to the `dest` field, and `dest` back to `src` in {Mapping#map_back}.
|
|
32
|
+
#
|
|
33
|
+
# @example
|
|
34
|
+
# map :email # email -> email
|
|
35
|
+
# map :email, :email_address # email -> email_address
|
|
36
|
+
# map :active, if_dest: :new_record?
|
|
37
|
+
#
|
|
38
|
+
# @param src [Symbol, String] field read from the source
|
|
39
|
+
# @param dest [Symbol, String] field written on the destination, defaults to `src`
|
|
40
|
+
# @param options [Hash] conditions (see {ClassMethods}); other keys are kept in {#mappings}
|
|
41
|
+
# @return [void]
|
|
42
|
+
def map(src, dest = nil, options = {})
|
|
43
|
+
if dest.is_a?(Hash)
|
|
44
|
+
options = dest
|
|
45
|
+
dest = nil
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
dest ||= src
|
|
49
|
+
|
|
50
|
+
options = ::Mappable::Mapping.default_mapping_options(src, dest).merge(options)
|
|
51
|
+
add_value_to_class_method(:mappings, dest.to_sym => options)
|
|
52
|
+
add_value_to_class_method(:map_back_mappings, src.to_sym => ::Mappable::Mapping.map_back_options(options))
|
|
53
|
+
compile_mappings
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Sets the `dest` field from a method of the mapping, or a block, given the source.
|
|
57
|
+
# Only applies to {Mapping#map}; use {#custom_map_back} for the reverse.
|
|
58
|
+
#
|
|
59
|
+
# @example
|
|
60
|
+
# custom_map :name # calls the mapping's name(src) method
|
|
61
|
+
# custom_map :name, :full_name # calls full_name(src)
|
|
62
|
+
# custom_map(:name) { |src| "#{src.first_name} #{src.last_name}" }
|
|
63
|
+
#
|
|
64
|
+
# @param dest [Symbol, String] field written on the destination
|
|
65
|
+
# @param custom_method [Symbol, String, Proc] defaults to the block, then to `dest`
|
|
66
|
+
# @param options [Hash] conditions (see {ClassMethods}); other keys are kept in {#mappings}
|
|
67
|
+
# @return [void]
|
|
68
|
+
def custom_map(dest, custom_method = nil, options = {}, &)
|
|
69
|
+
add_custom_mapping(:mappings, dest, custom_method, options, &)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Sets the `dest` field on the source object from a method of the mapping, or a block,
|
|
73
|
+
# given the destination object. Used by {Mapping#map_back}.
|
|
74
|
+
#
|
|
75
|
+
# @example
|
|
76
|
+
# custom_map_back(:first_name) { |contact| contact.name.split(' ', 2).first }
|
|
77
|
+
#
|
|
78
|
+
# @param dest [Symbol, String] field written on the source object
|
|
79
|
+
# @param custom_method [Symbol, String, Proc] defaults to the block, then to `dest`
|
|
80
|
+
# @param options [Hash] conditions (see {ClassMethods}); other keys are kept in {#map_back_mappings}
|
|
81
|
+
# @return [void]
|
|
82
|
+
def custom_map_back(dest, custom_method = nil, options = {}, &)
|
|
83
|
+
add_custom_mapping(:map_back_mappings, dest, custom_method, options, &)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Regenerates {Mapping#map} and {Mapping#map_back} from the current mappings.
|
|
87
|
+
# @api private
|
|
88
|
+
# @return [void]
|
|
89
|
+
def compile_mappings
|
|
90
|
+
compiler = ::Mappable::Compiler.new
|
|
91
|
+
source = [
|
|
92
|
+
compiler.method_source(:map, mappings),
|
|
93
|
+
compiler.method_source(:map_back, map_back_mappings)
|
|
94
|
+
].join("\n")
|
|
95
|
+
|
|
96
|
+
store_procs(compiler.procs)
|
|
97
|
+
%i[map map_back].each { |name| remove_method(name) if method_defined?(name, false) }
|
|
98
|
+
class_eval(source, __FILE__, __LINE__)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
private
|
|
102
|
+
|
|
103
|
+
def add_custom_mapping(method, dest, custom_method, options, &block)
|
|
104
|
+
if custom_method.is_a?(Hash)
|
|
105
|
+
options = custom_method
|
|
106
|
+
custom_method = nil
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
custom_method ||= block || dest
|
|
110
|
+
|
|
111
|
+
options = ::Mappable::Mapping.default_custom_mapping_options(dest, custom_method).merge(options)
|
|
112
|
+
add_value_to_class_method(method, dest.to_sym => options)
|
|
113
|
+
compile_mappings
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def store_procs(procs)
|
|
117
|
+
name = ::Mappable::Compiler::PROCS_CONSTANT
|
|
118
|
+
remove_const(name) if const_defined?(name, false)
|
|
119
|
+
const_set(name, procs.freeze)
|
|
120
|
+
private_constant(name)
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
data/lib/mappable/mapping.rb
CHANGED
|
@@ -1,79 +1,113 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Mappable
|
|
4
|
-
#
|
|
4
|
+
# Declares which fields are copied from one object to another, and back.
|
|
5
|
+
#
|
|
6
|
+
# Mapping classes are usually created by {Mappable::ClassMethods#map_to}, but any
|
|
7
|
+
# class can include this module and declare mappings with {ClassMethods#map},
|
|
8
|
+
# {ClassMethods#custom_map} and {ClassMethods#custom_map_back}. Each declaration
|
|
9
|
+
# regenerates the class's {#map} and {#map_back} methods (see {Compiler}).
|
|
10
|
+
#
|
|
11
|
+
# @example
|
|
12
|
+
# class ContactMapping
|
|
13
|
+
# include Mappable::Mapping
|
|
14
|
+
#
|
|
15
|
+
# map :email, :email_address
|
|
16
|
+
# custom_map(:name) { |user| "#{user.first_name} #{user.last_name}" }
|
|
17
|
+
# custom_map_back(:first_name) { |contact| contact.name.split(' ', 2).first }
|
|
18
|
+
# custom_map_back(:last_name) { |contact| contact.name.split(' ', 2).last }
|
|
19
|
+
# end
|
|
20
|
+
#
|
|
21
|
+
# ContactMapping.new.map(user, Contact.new) # => the contact
|
|
22
|
+
# ContactMapping.new.map_back(contact, User.new) # => the user
|
|
5
23
|
module Mapping
|
|
24
|
+
autoload :ClassMethods, 'mappable/mapping/class_methods'
|
|
25
|
+
|
|
26
|
+
# @api private
|
|
6
27
|
def self.included(base)
|
|
7
28
|
base.extend InheritanceHelper::Methods
|
|
8
29
|
base.extend ClassMethods
|
|
9
30
|
end
|
|
10
31
|
|
|
32
|
+
# Options for copying the `src` field into the `dest` field
|
|
33
|
+
# @api private
|
|
11
34
|
def self.default_mapping_options(src, dest)
|
|
12
35
|
{
|
|
13
36
|
src: src.to_sym,
|
|
14
|
-
|
|
15
|
-
src_setter: "#{src}=",
|
|
37
|
+
getter: src.to_s.freeze,
|
|
16
38
|
dest: dest.to_sym,
|
|
17
|
-
|
|
18
|
-
dest_setter: "#{dest}="
|
|
39
|
+
setter: "#{dest}="
|
|
19
40
|
}
|
|
20
41
|
end
|
|
21
42
|
|
|
22
|
-
#
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
options = dest
|
|
31
|
-
dest = nil
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
dest ||= src
|
|
35
|
-
|
|
36
|
-
options = ::Mappable::Mapping.default_mapping_options(src, dest)
|
|
37
|
-
.merge(options)
|
|
38
|
-
|
|
39
|
-
add_value_to_class_method(:mappings, src.to_sym => options)
|
|
40
|
-
end
|
|
43
|
+
# Options for setting the `dest` field from a custom method or proc
|
|
44
|
+
# @api private
|
|
45
|
+
def self.default_custom_mapping_options(dest, custom_method)
|
|
46
|
+
{
|
|
47
|
+
map_method: custom_method,
|
|
48
|
+
dest: dest.to_sym,
|
|
49
|
+
setter: "#{dest}="
|
|
50
|
+
}
|
|
41
51
|
end
|
|
42
52
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
53
|
+
# Each condition of a {ClassMethods#map} call and the condition it becomes in the
|
|
54
|
+
# reverse mapping. The source and destination swap places, so _src and _dest
|
|
55
|
+
# conditions swap too and still check the same object.
|
|
56
|
+
# @api private
|
|
57
|
+
MAP_BACK_CONDITIONS = {
|
|
58
|
+
if: :if,
|
|
59
|
+
unless: :unless,
|
|
60
|
+
if_src: :if_dest,
|
|
61
|
+
unless_src: :unless_dest,
|
|
62
|
+
if_dest: :if_src,
|
|
63
|
+
unless_dest: :unless_src
|
|
64
|
+
}.freeze
|
|
46
65
|
|
|
47
|
-
|
|
66
|
+
# Reverses the options of a {ClassMethods#map} call (see {MAP_BACK_CONDITIONS})
|
|
67
|
+
# @api private
|
|
68
|
+
def self.map_back_options(options)
|
|
69
|
+
new_options = default_mapping_options(options[:dest], options[:src])
|
|
70
|
+
MAP_BACK_CONDITIONS.each do |cond, map_back_cond|
|
|
71
|
+
new_options[map_back_cond] = options[cond] if options[cond]
|
|
48
72
|
end
|
|
49
|
-
|
|
73
|
+
new_options
|
|
50
74
|
end
|
|
51
75
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
76
|
+
# Creates a mapping class and sets it as a constant of `base_module`.
|
|
77
|
+
#
|
|
78
|
+
# @param base_module [Module] where the class's constant is set
|
|
79
|
+
# @param name [String, Symbol] the class is named after it: `:contact` becomes `ContactMapping`
|
|
80
|
+
# @param options [Hash]
|
|
81
|
+
# @option options [String] :class_name the class's name instead of one built from `name`
|
|
82
|
+
# @option options [Class] :base_class superclass, so a mapping can extend another one
|
|
83
|
+
# @yield evaluated in the class, to declare its mappings
|
|
84
|
+
# @return [Class]
|
|
85
|
+
def self.create(base_module, name, options = {}, &block)
|
|
86
|
+
class_name = options[:class_name] || "#{::Mappable::Utils.classify_name(name)}Mapping"
|
|
87
|
+
kls = base_module.const_set(class_name, Class.new(options[:base_class] || Object))
|
|
88
|
+
kls.include(::Mappable::Mapping)
|
|
89
|
+
kls.class_eval(&block) if block
|
|
90
|
+
kls
|
|
57
91
|
end
|
|
58
92
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
end
|
|
93
|
+
# Copies the mapped fields from `src_model` to `dest_model`. Replaced by a generated
|
|
94
|
+
# method once the class declares a mapping.
|
|
95
|
+
#
|
|
96
|
+
# @param _src_model [Object] not read: there is nothing to copy
|
|
97
|
+
# @param dest_model [Object]
|
|
98
|
+
# @return [Object] dest_model
|
|
99
|
+
def map(_src_model, dest_model)
|
|
100
|
+
dest_model
|
|
68
101
|
end
|
|
69
102
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
103
|
+
# Copies the fields of `dest_model` back to `src_model`, reversing {#map}. Replaced by a
|
|
104
|
+
# generated method once the class declares a mapping.
|
|
105
|
+
#
|
|
106
|
+
# @param _dest_model [Object] the object to read from, not read: there is nothing to copy
|
|
107
|
+
# @param src_model [Object] the object to write to
|
|
108
|
+
# @return [Object] src_model
|
|
109
|
+
def map_back(_dest_model, src_model)
|
|
110
|
+
src_model
|
|
77
111
|
end
|
|
78
112
|
end
|
|
79
113
|
end
|
data/lib/mappable/utils.rb
CHANGED
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Mappable
|
|
4
|
-
# General
|
|
4
|
+
# General purpose utility methods
|
|
5
5
|
module Utils
|
|
6
6
|
module_function
|
|
7
7
|
|
|
8
|
+
# Converts a mapping name into a class name: drops anything that isn't a letter,
|
|
9
|
+
# digit, underscore or dash, then camel cases the words split by underscores and dashes.
|
|
10
|
+
#
|
|
11
|
+
# @example
|
|
12
|
+
# Mappable::Utils.classify_name('user_profile') # => "UserProfile"
|
|
13
|
+
# Mappable::Utils.classify_name('UserProfile') # => "UserProfile"
|
|
14
|
+
#
|
|
15
|
+
# @param name [String, Symbol]
|
|
16
|
+
# @return [String]
|
|
8
17
|
def classify_name(name)
|
|
9
|
-
name.gsub(/[^\da-
|
|
18
|
+
name.to_s.gsub(/[^\da-zA-Z_-]/, '').gsub(/(?:\A|[_-]+)([\da-zA-Z])/) { ::Regexp.last_match(1).upcase }
|
|
10
19
|
end
|
|
11
20
|
end
|
|
12
21
|
end
|
data/lib/mappable.rb
CHANGED
|
@@ -2,34 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
require 'inheritance-helper'
|
|
4
4
|
|
|
5
|
-
#
|
|
5
|
+
# Maps data from one object to another and back.
|
|
6
|
+
#
|
|
7
|
+
# Include it in a class and declare mappings with {ClassMethods#map_to}.
|
|
6
8
|
module Mappable
|
|
9
|
+
autoload :ClassMethods, 'mappable/class_methods'
|
|
10
|
+
autoload :Compiler, 'mappable/compiler'
|
|
7
11
|
autoload :Mapping, 'mappable/mapping'
|
|
8
12
|
autoload :Utils, 'mappable/utils'
|
|
13
|
+
autoload :VERSION, 'mappable/version'
|
|
9
14
|
|
|
15
|
+
# @api private
|
|
10
16
|
def self.included(base)
|
|
11
17
|
base.extend InheritanceHelper::Methods
|
|
12
18
|
base.extend ClassMethods
|
|
13
19
|
end
|
|
14
|
-
|
|
15
|
-
# no-doc
|
|
16
|
-
module ClassMethods
|
|
17
|
-
def maps
|
|
18
|
-
{}.freeze
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
def map_to(name, options = {}, &block)
|
|
22
|
-
mapping = Mapping.create(self, name, options, &block)
|
|
23
|
-
add_value_to_class_method(:maps, name => mapping)
|
|
24
|
-
|
|
25
|
-
class_eval(
|
|
26
|
-
<<-STR, __FILE__, __LINE__ + 1
|
|
27
|
-
def map_to_#{name}(dest)
|
|
28
|
-
::#{mapping.name}.new.map(self, dest)
|
|
29
|
-
dest
|
|
30
|
-
end
|
|
31
|
-
STR
|
|
32
|
-
)
|
|
33
|
-
end
|
|
34
|
-
end
|
|
35
20
|
end
|
metadata
CHANGED
|
@@ -1,55 +1,59 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: model-mapper
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Doug Youch
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: inheritance-helper
|
|
15
14
|
requirement: !ruby/object:Gem::Requirement
|
|
16
15
|
requirements:
|
|
17
|
-
- - "
|
|
16
|
+
- - "~>"
|
|
18
17
|
- !ruby/object:Gem::Version
|
|
19
|
-
version: '0'
|
|
18
|
+
version: '0.2'
|
|
20
19
|
type: :runtime
|
|
21
20
|
prerelease: false
|
|
22
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
23
22
|
requirements:
|
|
24
|
-
- - "
|
|
23
|
+
- - "~>"
|
|
25
24
|
- !ruby/object:Gem::Version
|
|
26
|
-
version: '0'
|
|
27
|
-
description:
|
|
25
|
+
version: '0.2'
|
|
26
|
+
description: 'Declare once how fields map from one object to another, and Mappable
|
|
27
|
+
compiles it into plain Ruby methods that copy the data both ways at close to hand-written
|
|
28
|
+
speed. Rename fields, combine several into one (first_name + last_name -> name)
|
|
29
|
+
and split them back out, and skip fields with conditions on the source, the destination
|
|
30
|
+
or the mapping itself. Works with any objects that have getters and setters: Structs,
|
|
31
|
+
ActiveRecord models, plain Ruby classes.'
|
|
28
32
|
email: dougyouch@gmail.com
|
|
29
33
|
executables: []
|
|
30
34
|
extensions: []
|
|
31
35
|
extra_rdoc_files: []
|
|
32
36
|
files:
|
|
33
|
-
-
|
|
34
|
-
- ".rubocop.yml"
|
|
35
|
-
- ".ruby-gemset"
|
|
36
|
-
- ".ruby-version"
|
|
37
|
-
- ".travis.yml"
|
|
38
|
-
- Gemfile
|
|
39
|
-
- Gemfile.lock
|
|
37
|
+
- CHANGELOG.md
|
|
40
38
|
- LICENSE
|
|
41
39
|
- README.md
|
|
42
|
-
- gemfiles/travis.gemfile
|
|
43
40
|
- lib/mappable.rb
|
|
41
|
+
- lib/mappable/class_methods.rb
|
|
42
|
+
- lib/mappable/compiler.rb
|
|
44
43
|
- lib/mappable/mapping.rb
|
|
44
|
+
- lib/mappable/mapping/class_methods.rb
|
|
45
45
|
- lib/mappable/utils.rb
|
|
46
|
+
- lib/mappable/version.rb
|
|
46
47
|
- lib/model-mapper.rb
|
|
47
|
-
- model-mapper.gemspec
|
|
48
48
|
homepage: https://github.com/dougyouch/mappable
|
|
49
49
|
licenses:
|
|
50
50
|
- MIT
|
|
51
|
-
metadata:
|
|
52
|
-
|
|
51
|
+
metadata:
|
|
52
|
+
rubygems_mfa_required: 'true'
|
|
53
|
+
source_code_uri: https://github.com/dougyouch/mappable
|
|
54
|
+
changelog_uri: https://github.com/dougyouch/mappable/blob/master/CHANGELOG.md
|
|
55
|
+
bug_tracker_uri: https://github.com/dougyouch/mappable/issues
|
|
56
|
+
documentation_uri: https://rubydoc.info/gems/model-mapper
|
|
53
57
|
rdoc_options: []
|
|
54
58
|
require_paths:
|
|
55
59
|
- lib
|
|
@@ -57,16 +61,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
57
61
|
requirements:
|
|
58
62
|
- - ">="
|
|
59
63
|
- !ruby/object:Gem::Version
|
|
60
|
-
version: '
|
|
64
|
+
version: '3.2'
|
|
61
65
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
62
66
|
requirements:
|
|
63
67
|
- - ">="
|
|
64
68
|
- !ruby/object:Gem::Version
|
|
65
69
|
version: '0'
|
|
66
70
|
requirements: []
|
|
67
|
-
|
|
68
|
-
rubygems_version: 2.7.10
|
|
69
|
-
signing_key:
|
|
71
|
+
rubygems_version: 4.0.20
|
|
70
72
|
specification_version: 4
|
|
71
|
-
summary:
|
|
73
|
+
summary: Fast, declarative two-way mapping between Ruby objects
|
|
72
74
|
test_files: []
|
data/.gitignore
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
# rcov generated
|
|
2
|
-
coverage
|
|
3
|
-
coverage.data
|
|
4
|
-
|
|
5
|
-
# rdoc generated
|
|
6
|
-
rdoc
|
|
7
|
-
|
|
8
|
-
# yard generated
|
|
9
|
-
doc
|
|
10
|
-
.yardoc
|
|
11
|
-
|
|
12
|
-
# bundler
|
|
13
|
-
.bundle
|
|
14
|
-
|
|
15
|
-
# jeweler generated
|
|
16
|
-
pkg
|
|
17
|
-
|
|
18
|
-
# Have editor/IDE/OS specific files you need to ignore? Consider using a global gitignore:
|
|
19
|
-
#
|
|
20
|
-
# * Create a file at ~/.gitignore
|
|
21
|
-
# * Include files you want ignored
|
|
22
|
-
# * Run: git config --global core.excludesfile ~/.gitignore
|
|
23
|
-
#
|
|
24
|
-
# After doing this, these files will be ignored in all your git projects,
|
|
25
|
-
# saving you from having to 'pollute' every project you touch with them
|
|
26
|
-
#
|
|
27
|
-
# Not sure what to needs to be ignored for particular editors/OSes? Here's some ideas to get you started. (Remember, remove the leading # of the line)
|
|
28
|
-
#
|
|
29
|
-
# For MacOS:
|
|
30
|
-
#
|
|
31
|
-
.DS_Store
|
|
32
|
-
|
|
33
|
-
# For TextMate
|
|
34
|
-
#*.tmproj
|
|
35
|
-
#tmtags
|
|
36
|
-
|
|
37
|
-
# For emacs:
|
|
38
|
-
*~
|
|
39
|
-
\#*
|
|
40
|
-
.\#*
|
|
41
|
-
|
|
42
|
-
# For vim:
|
|
43
|
-
*.swp
|
|
44
|
-
|
|
45
|
-
# For redcar:
|
|
46
|
-
#.redcar
|
|
47
|
-
|
|
48
|
-
# For rubinius:
|
|
49
|
-
#*.rbc
|
|
50
|
-
|
|
51
|
-
*.gem
|
data/.rubocop.yml
DELETED
data/.ruby-gemset
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
mappable
|
data/.ruby-version
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
2.6.3
|
data/.travis.yml
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
env:
|
|
2
|
-
global:
|
|
3
|
-
- CC_TEST_REPORTER_ID=f54f3242f54a3040953c9127294fef862cb1851d12333d004619a8b7b7842054
|
|
4
|
-
rvm:
|
|
5
|
-
- 2.6.3
|
|
6
|
-
- 2.1.9
|
|
7
|
-
- 1.9.3
|
|
8
|
-
gemfile: gemfiles/travis.gemfile
|
|
9
|
-
language: ruby
|
|
10
|
-
before_script:
|
|
11
|
-
- curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
|
|
12
|
-
- chmod +x ./cc-test-reporter
|
|
13
|
-
- ./cc-test-reporter before-build
|
|
14
|
-
script:
|
|
15
|
-
- bundle exec rspec
|
|
16
|
-
after_script:
|
|
17
|
-
- ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT
|
data/Gemfile
DELETED
data/Gemfile.lock
DELETED
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
GEM
|
|
2
|
-
remote: http://rubygems.org/
|
|
3
|
-
specs:
|
|
4
|
-
ast (2.4.0)
|
|
5
|
-
diff-lcs (1.3)
|
|
6
|
-
docile (1.3.2)
|
|
7
|
-
inheritance-helper (0.1.0)
|
|
8
|
-
jaro_winkler (1.5.3)
|
|
9
|
-
json (2.2.0)
|
|
10
|
-
parallel (1.17.0)
|
|
11
|
-
parser (2.6.4.0)
|
|
12
|
-
ast (~> 2.4.0)
|
|
13
|
-
rainbow (3.0.0)
|
|
14
|
-
rake (12.3.3)
|
|
15
|
-
rspec (3.8.0)
|
|
16
|
-
rspec-core (~> 3.8.0)
|
|
17
|
-
rspec-expectations (~> 3.8.0)
|
|
18
|
-
rspec-mocks (~> 3.8.0)
|
|
19
|
-
rspec-core (3.8.2)
|
|
20
|
-
rspec-support (~> 3.8.0)
|
|
21
|
-
rspec-expectations (3.8.4)
|
|
22
|
-
diff-lcs (>= 1.2.0, < 2.0)
|
|
23
|
-
rspec-support (~> 3.8.0)
|
|
24
|
-
rspec-mocks (3.8.1)
|
|
25
|
-
diff-lcs (>= 1.2.0, < 2.0)
|
|
26
|
-
rspec-support (~> 3.8.0)
|
|
27
|
-
rspec-support (3.8.2)
|
|
28
|
-
rubocop (0.74.0)
|
|
29
|
-
jaro_winkler (~> 1.5.1)
|
|
30
|
-
parallel (~> 1.10)
|
|
31
|
-
parser (>= 2.6)
|
|
32
|
-
rainbow (>= 2.2.2, < 4.0)
|
|
33
|
-
ruby-progressbar (~> 1.7)
|
|
34
|
-
unicode-display_width (>= 1.4.0, < 1.7)
|
|
35
|
-
ruby-progressbar (1.10.1)
|
|
36
|
-
simplecov (0.17.0)
|
|
37
|
-
docile (~> 1.1)
|
|
38
|
-
json (>= 1.8, < 3)
|
|
39
|
-
simplecov-html (~> 0.10.0)
|
|
40
|
-
simplecov-html (0.10.2)
|
|
41
|
-
unicode-display_width (1.6.0)
|
|
42
|
-
|
|
43
|
-
PLATFORMS
|
|
44
|
-
ruby
|
|
45
|
-
|
|
46
|
-
DEPENDENCIES
|
|
47
|
-
inheritance-helper
|
|
48
|
-
rake
|
|
49
|
-
rspec
|
|
50
|
-
rubocop
|
|
51
|
-
simplecov
|
|
52
|
-
|
|
53
|
-
BUNDLED WITH
|
|
54
|
-
1.17.3
|
data/gemfiles/travis.gemfile
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
gemfiles/../Gemfile
|
data/model-mapper.gemspec
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
Gem::Specification.new do |s|
|
|
4
|
-
s.name = 'model-mapper'
|
|
5
|
-
s.version = '0.1.0'
|
|
6
|
-
s.licenses = ['MIT']
|
|
7
|
-
s.summary = 'Map data between models'
|
|
8
|
-
s.description = 'Easy way to configure what data is mapped between models'
|
|
9
|
-
s.authors = ['Doug Youch']
|
|
10
|
-
s.email = 'dougyouch@gmail.com'
|
|
11
|
-
s.homepage = 'https://github.com/dougyouch/mappable'
|
|
12
|
-
s.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features|script)/}) }
|
|
13
|
-
|
|
14
|
-
s.add_runtime_dependency 'inheritance-helper'
|
|
15
|
-
end
|