rails_view_adapters 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +10 -0
- data/.rspec +2 -0
- data/.rubocop.yml +181 -0
- data/.travis.yml +7 -0
- data/Gemfile +4 -0
- data/LICENSE.md +27 -0
- data/README.md +88 -0
- data/Rakefile +6 -0
- data/lib/rails_view_adapters/adapter.rb +34 -0
- data/lib/rails_view_adapters/adapter_base.rb +101 -0
- data/lib/rails_view_adapters/definition_proxy.rb +155 -0
- data/lib/rails_view_adapters/map.rb +46 -0
- data/lib/rails_view_adapters/version.rb +4 -0
- data/lib/rails_view_adapters.rb +13 -0
- data/rails_view_adapters.gemspec +35 -0
- data/spec/active_record_helper.rb +16 -0
- data/spec/active_record_support/db/migrate/20161031222934_create_users.rb +14 -0
- data/spec/active_record_support/db/migrate/20161031223219_create_posts.rb +11 -0
- data/spec/active_record_support/db/migrate/20161031223253_create_teams.rb +10 -0
- data/spec/active_record_support/fabricators/post_fabricator.rb +7 -0
- data/spec/active_record_support/fabricators/team_fabricator.rb +6 -0
- data/spec/active_record_support/fabricators/user_fabricator.rb +10 -0
- data/spec/active_record_support/models/post.rb +4 -0
- data/spec/active_record_support/models/team.rb +4 -0
- data/spec/active_record_support/models/user.rb +5 -0
- data/spec/integration/an_adapter_spec.rb +58 -0
- data/spec/lib/adapter_base_spec.rb +133 -0
- data/spec/lib/adapter_spec.rb +45 -0
- data/spec/lib/definition_proxy_spec.rb +242 -0
- data/spec/lib/map_spec.rb +99 -0
- data/spec/spec_helper.rb +19 -0
- data/spec/support/an_adapter.rb +72 -0
- data/spec/support/fuzzy_nested_match.rb +96 -0
- metadata +206 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 271a96685da34e2fef21042413b046c05bd3b48a
|
4
|
+
data.tar.gz: 9dcb4018a390cec96a55c604435ddbde14ccd023
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 141a9618d9417e8fe45d72863d7c532489d16227ea51ec290657a52f4bdec96d3951356e09af831b0b8fe93ebbf6e3d694545c03c6468565d4d040fb7e61d0db
|
7
|
+
data.tar.gz: b0d4a193b1e85d9d8d563a7dbb999e14a2d8a78b9f82dd503174622ad555c285f2cc6da6d5153ca0eb33943aaecf78b2d286a8f923140c987b6150b913067796
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.rubocop.yml
ADDED
@@ -0,0 +1,181 @@
|
|
1
|
+
# Copyright (c) 2015 The Regents of the University of Michigan.
|
2
|
+
# All Rights Reserved.
|
3
|
+
# Licensed according to the terms of the Revised BSD License
|
4
|
+
# See LICENSE.md for details.
|
5
|
+
|
6
|
+
AllCops:
|
7
|
+
DisplayCopNames: true
|
8
|
+
TargetRubyVersion: 2.3
|
9
|
+
|
10
|
+
|
11
|
+
Style/Alias:
|
12
|
+
EnforcedStyle: prefer_alias_method
|
13
|
+
|
14
|
+
Metrics/LineLength:
|
15
|
+
Max: 100
|
16
|
+
AllowHeredoc: true
|
17
|
+
AllowURI: true
|
18
|
+
URISchemes:
|
19
|
+
- http
|
20
|
+
- https
|
21
|
+
|
22
|
+
Style/FirstParameterIndentation:
|
23
|
+
EnforcedStyle: consistent
|
24
|
+
|
25
|
+
Style/AlignParameters:
|
26
|
+
# Alignment of parameters in multi-line method calls.
|
27
|
+
#
|
28
|
+
# The `with_first_parameter` style aligns the following lines along the same
|
29
|
+
# column as the first parameter.
|
30
|
+
#
|
31
|
+
# method_call(a,
|
32
|
+
# b)
|
33
|
+
#
|
34
|
+
# The `with_fixed_indentation` style aligns the following lines with one
|
35
|
+
# level of indentation relative to the start of the line with the method call.
|
36
|
+
#
|
37
|
+
# method_call(a,
|
38
|
+
# b)
|
39
|
+
EnforcedStyle: with_fixed_indentation
|
40
|
+
|
41
|
+
# Indentation of `when`.
|
42
|
+
Style/CaseIndentation:
|
43
|
+
IndentWhenRelativeTo: end
|
44
|
+
|
45
|
+
# This usually but not always does what we want; disable in individual places
|
46
|
+
# where it gets it wrong
|
47
|
+
Style/ClassAndModuleChildren:
|
48
|
+
EnforcedStyle: nested
|
49
|
+
|
50
|
+
# Checks formatting of special comments
|
51
|
+
Style/CommentAnnotation:
|
52
|
+
Enabled: false
|
53
|
+
|
54
|
+
Style/Copyright:
|
55
|
+
Enabled: false
|
56
|
+
|
57
|
+
Style/EmptyLineBetweenDefs:
|
58
|
+
# If true, this parameter means that single line method definitions don't
|
59
|
+
# need an empty line between them.
|
60
|
+
AllowAdjacentOneLineDefs: true
|
61
|
+
|
62
|
+
Style/EmptyLinesAroundClassBody:
|
63
|
+
Enabled: false
|
64
|
+
|
65
|
+
Style/EmptyLinesAroundModuleBody:
|
66
|
+
Enabled: false
|
67
|
+
|
68
|
+
Style/FileName:
|
69
|
+
# When true, requires that each source file should define a class or module
|
70
|
+
# with a name which matches the file name (converted to ... case).
|
71
|
+
# It further expects it to be nested inside modules which match the names
|
72
|
+
# of subdirectories in its path.
|
73
|
+
ExpectMatchingDefinition: false
|
74
|
+
|
75
|
+
Style/GuardClause:
|
76
|
+
Enabled: false
|
77
|
+
|
78
|
+
Style/IfUnlessModifier:
|
79
|
+
MaxLineLength: 100
|
80
|
+
|
81
|
+
# Checks the indentation of the first element in an array literal.
|
82
|
+
Style/IndentArray:
|
83
|
+
# The value `special_inside_parentheses` means that array literals with
|
84
|
+
# brackets that have their opening bracket on the same line as a surrounding
|
85
|
+
# opening round parenthesis, shall have their first element indented relative
|
86
|
+
# to the first position inside the parenthesis.
|
87
|
+
#
|
88
|
+
# The value `consistent` means that the indentation of the first element shall
|
89
|
+
# always be relative to the first position of the line where the opening
|
90
|
+
# bracket is.
|
91
|
+
#
|
92
|
+
# The value `align_brackets` means that the indentation of the first element
|
93
|
+
# shall always be relative to the position of the opening bracket.
|
94
|
+
EnforcedStyle: consistent
|
95
|
+
|
96
|
+
# Checks the indentation of the first key in a hash literal.
|
97
|
+
Style/IndentHash:
|
98
|
+
# The value `special_inside_parentheses` means that hash literals with braces
|
99
|
+
# that have their opening brace on the same line as a surrounding opening
|
100
|
+
# round parenthesis, shall have their first key indented relative to the
|
101
|
+
# first position inside the parenthesis.
|
102
|
+
#
|
103
|
+
# The value `consistent` means that the indentation of the first key shall
|
104
|
+
# always be relative to the first position of the line where the opening
|
105
|
+
# brace is.
|
106
|
+
#
|
107
|
+
# The value `align_braces` means that the indentation of the first key shall
|
108
|
+
# always be relative to the position of the opening brace.
|
109
|
+
EnforcedStyle: consistent
|
110
|
+
|
111
|
+
|
112
|
+
Style/MultilineMethodCallIndentation:
|
113
|
+
EnforcedStyle: indented
|
114
|
+
|
115
|
+
Style/MultilineOperationIndentation:
|
116
|
+
EnforcedStyle: indented
|
117
|
+
|
118
|
+
Style/Next:
|
119
|
+
Enabled: false
|
120
|
+
|
121
|
+
Style/RedundantReturn:
|
122
|
+
# When true allows code like `return x, y`.
|
123
|
+
AllowMultipleReturnValues: true
|
124
|
+
|
125
|
+
# Use / or %r around regular expressions.
|
126
|
+
Style/RegexpLiteral:
|
127
|
+
# If false, the cop will always recommend using %r if one or more slashes
|
128
|
+
# are found in the regexp string.
|
129
|
+
AllowInnerSlashes: true
|
130
|
+
|
131
|
+
Style/Semicolon:
|
132
|
+
# Allow ; to separate several expressions on the same line.
|
133
|
+
AllowAsExpressionSeparator: true
|
134
|
+
|
135
|
+
Style/StringLiterals:
|
136
|
+
EnforcedStyle: double_quotes
|
137
|
+
|
138
|
+
Style/StringLiteralsInInterpolation:
|
139
|
+
EnforcedStyle: double_quotes
|
140
|
+
|
141
|
+
Style/SpaceInsideBlockBraces:
|
142
|
+
SpaceBeforeBlockParameters: false
|
143
|
+
|
144
|
+
Style/SymbolArray:
|
145
|
+
EnforcedStyle: brackets
|
146
|
+
|
147
|
+
Style/WhileUntilModifier:
|
148
|
+
MaxLineLength: 100
|
149
|
+
|
150
|
+
# checks whether the end keywords are aligned properly for `do` `end` blocks.
|
151
|
+
Lint/BlockAlignment:
|
152
|
+
# The value `start_of_block` means that the `end` should be aligned with line
|
153
|
+
# where the `do` keyword appears.
|
154
|
+
# The value `start_of_line` means it should be aligned with the whole
|
155
|
+
# expression's starting line.
|
156
|
+
# The value `either` means both are allowed.
|
157
|
+
AlignWith: start_of_line
|
158
|
+
|
159
|
+
# Align ends correctly.
|
160
|
+
Lint/EndAlignment:
|
161
|
+
# The value `keyword` means that `end` should be aligned with the matching
|
162
|
+
# keyword (if, while, etc.).
|
163
|
+
# The value `variable` means that in assignments, `end` should be aligned
|
164
|
+
# with the start of the variable on the left hand side of `=`. In all other
|
165
|
+
# situations, `end` should still be aligned with the keyword.
|
166
|
+
# The value `start_of_line` means that `end` should be aligned with the start
|
167
|
+
# of the line which the matching keyword appears on.
|
168
|
+
AlignWith: start_of_line
|
169
|
+
|
170
|
+
Lint/DefEndAlignment:
|
171
|
+
# The value `def` means that `end` should be aligned with the def keyword.
|
172
|
+
# The value `start_of_line` means that `end` should be aligned with method
|
173
|
+
# calls like `private`, `public`, etc, if present in front of the `def`
|
174
|
+
# keyword on the same line.
|
175
|
+
AlignWith: def
|
176
|
+
|
177
|
+
Performance/RedundantMerge:
|
178
|
+
Enabled: false
|
179
|
+
|
180
|
+
Style/WordArray:
|
181
|
+
EnforcedStyle: brackets
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.md
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
Copyright (c) 2015, The Regents of the University of Michigan.
|
2
|
+
All rights reserved.
|
3
|
+
|
4
|
+
Redistribution and use in source and binary forms, with or without
|
5
|
+
modification, are permitted provided that the following conditions are
|
6
|
+
met:
|
7
|
+
|
8
|
+
* Redistributions of source code must retain the above copyright
|
9
|
+
notice, this list of conditions and the following disclaimer.
|
10
|
+
* Redistributions in binary form must reproduce the above copyright
|
11
|
+
notice, this list of conditions and the following disclaimer in the
|
12
|
+
documentation and/or other materials provided with the distribution.
|
13
|
+
* Neither the name of the The University of Michigan nor the
|
14
|
+
names of its contributors may be used to endorse or promote products
|
15
|
+
derived from this software without specific prior written permission.
|
16
|
+
|
17
|
+
THIS SOFTWARE IS PROVIDED BY THE REGENTS OF THE UNIVERSITY OF MICHIGAN AND
|
18
|
+
CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT
|
19
|
+
NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
20
|
+
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OF THE
|
21
|
+
UNIVERSITY OF MICHIGAN BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
22
|
+
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
|
23
|
+
TO,PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
24
|
+
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
25
|
+
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
26
|
+
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
|
27
|
+
EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
data/README.md
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
# Rails View Adapters
|
2
|
+
|
3
|
+
This gem provides a
|
4
|
+
[DSL](http://www.rubydoc.info/github/mlibrary/rails_view_adapters/master/RailsViewAdapters/DefinitionProxy)
|
5
|
+
for defining adapters that map your model's
|
6
|
+
representations to those of your views, and vice versa.
|
7
|
+
|
8
|
+
### Adapters are presenters
|
9
|
+
|
10
|
+
The adapters can be used to convert a model to its public representation,
|
11
|
+
including associated models, as well as support for arbitrary operations.
|
12
|
+
|
13
|
+
```ruby
|
14
|
+
FooAdapter.from_model(Foo.find(id)).to_public_hash
|
15
|
+
```
|
16
|
+
|
17
|
+
### Adapters wrap input parameters
|
18
|
+
|
19
|
+
The adapters also understand how to "undo" the presentation logic,
|
20
|
+
converting the public representation back to the model or models
|
21
|
+
that (may have) generated it.
|
22
|
+
|
23
|
+
```ruby
|
24
|
+
Bar.new(BarAdapter.from_public(params).to_params_hash)
|
25
|
+
```
|
26
|
+
|
27
|
+
## Usage
|
28
|
+
|
29
|
+
### Basics
|
30
|
+
|
31
|
+
Define your adapter using the DSL.
|
32
|
+
|
33
|
+
```ruby
|
34
|
+
# lib/adapters/team_member_adapter.rb
|
35
|
+
require "rails_view_adapters"
|
36
|
+
RailsViewAdapters::Adapter.define(:team_member_adapter) do
|
37
|
+
map_simple :name, :author
|
38
|
+
map_date :join_date, :member_since, date_format
|
39
|
+
map_date :created_at, :created_at, date_format
|
40
|
+
map_date :updated_at, :updated_at, date_format
|
41
|
+
map_bool :admin, :super_user
|
42
|
+
hidden_field :secret
|
43
|
+
map_from_public :secret do |token|
|
44
|
+
{ secret: token }
|
45
|
+
end
|
46
|
+
map_belongs_to :team, :favorite_team, model_class: Team
|
47
|
+
map_has_many :posts, :all_posts, sub_method: :body
|
48
|
+
end
|
49
|
+
```
|
50
|
+
|
51
|
+
Then require and use it like you would any other class.
|
52
|
+
|
53
|
+
```ruby
|
54
|
+
require "lib/adapters/team_member_adapter"
|
55
|
+
TeamMemberAdapter.from_model(TeamMember.find(params[:id])).to_public_hash
|
56
|
+
```
|
57
|
+
|
58
|
+
### Rails
|
59
|
+
|
60
|
+
I've found it convenient to create a concern that gets invoked in a controller
|
61
|
+
`before_action` to automatically grab the right adapter, instantiate it, and use
|
62
|
+
it to modify the params hash. Something like this:
|
63
|
+
|
64
|
+
```ruby
|
65
|
+
module Adaptation
|
66
|
+
extend ActiveSupport::Concern
|
67
|
+
|
68
|
+
included do
|
69
|
+
append_before_action :adapt_params
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
def adapt_params
|
74
|
+
adapter = "#{controller_path.classify.gsub("Controller", "")}Adapter".constantize
|
75
|
+
params.merge!(adapter.from_public(params).to_params_hash) {|key,lhs,rhs| rhs}
|
76
|
+
end
|
77
|
+
end
|
78
|
+
```
|
79
|
+
|
80
|
+
### Testing Adapters
|
81
|
+
|
82
|
+
Individual needs will vary, but for a reasonable integration test take a look at
|
83
|
+
`spec/integration/an_adapter_spec.rb`
|
84
|
+
|
85
|
+
Do note that the above relies on your controllers and adapters being named predictably.
|
86
|
+
|
87
|
+
## Documentation
|
88
|
+
http://www.rubydoc.info/github/mlibrary/rails_view_adapters/
|
data/Rakefile
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require "rails_view_adapters/definition_proxy"
|
3
|
+
require "rails_view_adapters/map"
|
4
|
+
require "rails_view_adapters/adapter_base"
|
5
|
+
|
6
|
+
module RailsViewAdapters
|
7
|
+
|
8
|
+
# Top level namespace for defining the adapters.
|
9
|
+
module Adapter
|
10
|
+
|
11
|
+
FIELDS = [
|
12
|
+
:model_fields, :public_fields,
|
13
|
+
:to_maps, :from_maps, :simple_maps
|
14
|
+
].freeze
|
15
|
+
|
16
|
+
def self.define(name, &block)
|
17
|
+
proxy = DefinitionProxy.new(Map.new)
|
18
|
+
proxy.instance_eval(&block)
|
19
|
+
Object.const_set(name.to_s.classify, adapter_from_map(proxy.map))
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.adapter_from_map(map)
|
23
|
+
Class.new(AdapterBase) do
|
24
|
+
FIELDS.each do |method|
|
25
|
+
define_singleton_method method do
|
26
|
+
map.send(method)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
# Copyright (c) 2015 The Regents of the University of Michigan.
|
3
|
+
# All Rights Reserved.
|
4
|
+
# Licensed according to the terms of the Revised BSD License
|
5
|
+
# See LICENSE.md for details.
|
6
|
+
|
7
|
+
require "active_support/core_ext/hash"
|
8
|
+
|
9
|
+
module RailsViewAdapters
|
10
|
+
|
11
|
+
# Base class on which adapters are defined.
|
12
|
+
class AdapterBase
|
13
|
+
# Create an instance from an ActiveRecord model.
|
14
|
+
# @param [ActiveRecord::AdapterBase] model
|
15
|
+
# @return the adapter
|
16
|
+
def self.from_model(model)
|
17
|
+
internals = {}
|
18
|
+
model_fields.each do |field|
|
19
|
+
internals[field] = model.send(field)
|
20
|
+
end
|
21
|
+
new(internals, {})
|
22
|
+
end
|
23
|
+
|
24
|
+
# Create an instance from a public representation.
|
25
|
+
# @param [ActionController::Parameters] public
|
26
|
+
# @return the adapter
|
27
|
+
def self.from_public(public)
|
28
|
+
internals = {}
|
29
|
+
simple_maps.each do |model_field, public_field|
|
30
|
+
internals[model_field] = public[public_field]
|
31
|
+
end
|
32
|
+
|
33
|
+
extras = {}
|
34
|
+
(public.keys.map(&:to_sym) - public_fields).each do |extra_key|
|
35
|
+
extras[extra_key] = public[extra_key]
|
36
|
+
end
|
37
|
+
|
38
|
+
from_maps.each do |public_field, process|
|
39
|
+
internals.merge!(process.call(public[public_field]))
|
40
|
+
end
|
41
|
+
|
42
|
+
new(internals, extras)
|
43
|
+
end
|
44
|
+
|
45
|
+
def initialize(internals, extras)
|
46
|
+
@internals = internals
|
47
|
+
@extras = extras
|
48
|
+
@public_hash = nil
|
49
|
+
@params_hash = nil
|
50
|
+
end
|
51
|
+
|
52
|
+
def to_params_hash
|
53
|
+
@params_hash ||= to_model_hash.merge(@extras.symbolize_keys) {|_key, lhs, _rhs| lhs }
|
54
|
+
end
|
55
|
+
|
56
|
+
def to_json(options = {})
|
57
|
+
to_public_hash.to_json(options)
|
58
|
+
end
|
59
|
+
|
60
|
+
def to_model_hash
|
61
|
+
@internals
|
62
|
+
end
|
63
|
+
|
64
|
+
def to_public_hash
|
65
|
+
unless @public_hash
|
66
|
+
@public_hash = {}
|
67
|
+
simple_maps.each do |model_field, public_field|
|
68
|
+
@public_hash[public_field] = @internals[model_field]
|
69
|
+
end
|
70
|
+
to_maps.each do |model_field, process|
|
71
|
+
@public_hash.merge!(process.call(@internals[model_field])) do |k, l, r|
|
72
|
+
merge_strategy.call(k, l, r)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
@public_hash
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
# Define an instance method for each of the class methods
|
82
|
+
# we want to access, otherwise we have to prepend
|
83
|
+
# "self.class." each time.
|
84
|
+
[:simple_maps, :to_maps].each do |method_name|
|
85
|
+
define_method(method_name) do
|
86
|
+
self.class.send(method_name)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def merge_strategy
|
91
|
+
@merge_strategy ||= proc do |_key, lhs, rhs|
|
92
|
+
if lhs.respond_to?(:merge) && rhs.respond_to?(:merge)
|
93
|
+
lhs.merge(rhs)
|
94
|
+
else
|
95
|
+
rhs
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,155 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module RailsViewAdapters
|
3
|
+
|
4
|
+
# Defines the DSL methods that are used to modify the underlying
|
5
|
+
# map. This class is only used to evaluate the DSL calls, thereby
|
6
|
+
# modifying the Map.
|
7
|
+
class DefinitionProxy
|
8
|
+
attr_accessor :map
|
9
|
+
def initialize(adapter_map)
|
10
|
+
@map = adapter_map
|
11
|
+
end
|
12
|
+
|
13
|
+
# Register a simple one-to-one mapping.
|
14
|
+
# @param [Symbol] model_field
|
15
|
+
# @param [Symbol] public_field
|
16
|
+
def map_simple(model_field, public_field)
|
17
|
+
map.add_simple_map(model_field, public_field)
|
18
|
+
end
|
19
|
+
|
20
|
+
# Register a mapping from a model field to the public representation.
|
21
|
+
# @param [Symbol] model_field
|
22
|
+
# @param [Array<Symbol>] extra_public_fields Used to tell the adapter about
|
23
|
+
# extra public fields created by this mapping.
|
24
|
+
# @yield [model_value] Given the value of the model's model_field, return
|
25
|
+
# a hash of key:values pairs to merge into the public representation.
|
26
|
+
def map_to_public(model_field, extra_public_fields = [], &block)
|
27
|
+
map.add_to_map(model_field, &block)
|
28
|
+
extra_public_fields.each do |public_field|
|
29
|
+
map.add_public_field(public_field)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# Register a mapping from a public field to the model representation.
|
34
|
+
# @param [Symbol] public_field
|
35
|
+
# @yield [public_value] Given the value of public representation's
|
36
|
+
# public_field, return a hash of key:value pairs to merge into
|
37
|
+
# the internal model representation.
|
38
|
+
def map_from_public(public_field, &block)
|
39
|
+
map.add_from_map(public_field, &block)
|
40
|
+
end
|
41
|
+
|
42
|
+
# Register a hidden field, i.e. a field not present in public representations.
|
43
|
+
# @param [Symbol] model_field
|
44
|
+
def hidden_field(model_field)
|
45
|
+
map.add_model_field(model_field)
|
46
|
+
end
|
47
|
+
|
48
|
+
# Register a one-to-one mapping of a date field.
|
49
|
+
# @param [Symbol] model_field
|
50
|
+
# @param [Symbol] public_field
|
51
|
+
# @param [String] date_format The Date format to use.
|
52
|
+
def map_date(model_field, public_field, date_format)
|
53
|
+
raise ArgumentError if date_format.nil?
|
54
|
+
map_from_public public_field do |value|
|
55
|
+
{ model_field => time_from_public(value) }
|
56
|
+
end
|
57
|
+
map_to_public model_field do |value|
|
58
|
+
{ public_field => value.utc.strftime(date_format) }
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# Register a one-to-one mapping of a boolean field
|
63
|
+
# @param [Symbol] model_field
|
64
|
+
# @param [Symbol] public_field
|
65
|
+
def map_bool(model_field, public_field)
|
66
|
+
map_from_public public_field do |value|
|
67
|
+
{ model_field => to_bool(value) }
|
68
|
+
end
|
69
|
+
|
70
|
+
map_to_public model_field do |value|
|
71
|
+
{ public_field => value }
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# Register a mapping of a belongs_to association.
|
76
|
+
# @param [Symbol] model_field The field on the model that holds the association,
|
77
|
+
# usually the association's name.
|
78
|
+
# @param [Symbol] public_field The public field.
|
79
|
+
# @param [Hash] options
|
80
|
+
# @option options [Class] :model_class The class of the associated model,
|
81
|
+
# if it cannot be inferred from the model_field.
|
82
|
+
# @option options [Symbol] :sub_method The method of the association model
|
83
|
+
# that holds the desired data. Default is :id.
|
84
|
+
# @option options [Symbol] :only Only create the to_map or the from_map, as
|
85
|
+
# directed by setting this to :to or :from, respectively.
|
86
|
+
def map_belongs_to(model_field, public_field, options = {})
|
87
|
+
model_class = options[:model_class] || model_field.to_s.classify.constantize
|
88
|
+
sub_method = options[:sub_method] || :id
|
89
|
+
model_field_id = :"#{model_field.to_s.sub(/(_id|)\Z/, "_id")}"
|
90
|
+
|
91
|
+
unless options[:only] == :to
|
92
|
+
map_from_public public_field do |value|
|
93
|
+
record = model_class.send(:"find_by_#{sub_method}", value)
|
94
|
+
{ model_field_id => record ? record.id : nil }
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
unless options[:only] == :from
|
99
|
+
map_to_public model_field_id do |id|
|
100
|
+
{ public_field => model_class.find_by(id: id).send(sub_method) }
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
# Register a mapping of a has_many association.
|
106
|
+
# @param [Symbol] model_field The field on the model that holds the association,
|
107
|
+
# usually the association's name.
|
108
|
+
# @param [Symbol] public_field The public field.
|
109
|
+
# @param [Hash] options
|
110
|
+
# @option options [Class] :model_class The class of the model, if it cannot
|
111
|
+
# be inferred from the model_field.
|
112
|
+
# @option options [Symbol] :sub_method The method of the association model
|
113
|
+
# that holds the desired data. If this isn't provided, it's assumed
|
114
|
+
# to be the same as public_field.
|
115
|
+
def map_has_many(model_field, public_field, options = {})
|
116
|
+
model_class = options[:model_class] || model_field.to_s.classify.constantize
|
117
|
+
sub_method = options[:sub_method] || public_field
|
118
|
+
|
119
|
+
unless options[:only] == :to
|
120
|
+
map_from_public public_field do |value|
|
121
|
+
result = { model_field => model_class.where(sub_method => value) }
|
122
|
+
public_field_size = value.respond_to?(:size) ? value.size : 0
|
123
|
+
result[model_field] = result[model_field]
|
124
|
+
.to_a
|
125
|
+
.fill(nil, result[model_field].size, public_field_size - result[model_field].size)
|
126
|
+
result
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
unless options[:only] == :from
|
131
|
+
map_to_public model_field do |records|
|
132
|
+
{ public_field => records.map(&sub_method.to_sym) }
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
private
|
138
|
+
|
139
|
+
def time_from_public(time)
|
140
|
+
if time.is_a? String
|
141
|
+
Time.zone.parse(time)
|
142
|
+
else
|
143
|
+
time
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
def to_bool(value)
|
148
|
+
return nil if value.nil? || value =~ /^(null|nil)$/i
|
149
|
+
return true if value == true || value =~ /^(true|t|yes|y|1)$/i
|
150
|
+
return false if value == false || value =~ /^(false|f|no|n|0)$/i
|
151
|
+
raise ArgumentError, "invalid value for boolean: \"#{value}\""
|
152
|
+
end
|
153
|
+
|
154
|
+
end
|
155
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module RailsViewAdapters
|
3
|
+
|
4
|
+
# Contains the information needed by the adapters
|
5
|
+
# to convert from one form to another.
|
6
|
+
class Map
|
7
|
+
attr_accessor :model_fields, :public_fields
|
8
|
+
attr_accessor :simple_maps, :to_maps, :from_maps
|
9
|
+
def initialize
|
10
|
+
@simple_maps = []
|
11
|
+
@model_fields = []
|
12
|
+
@public_fields = []
|
13
|
+
@to_maps = []
|
14
|
+
@from_maps = []
|
15
|
+
end
|
16
|
+
|
17
|
+
def add_model_field(model_field)
|
18
|
+
model_fields << model_field
|
19
|
+
self
|
20
|
+
end
|
21
|
+
|
22
|
+
def add_public_field(public_field)
|
23
|
+
public_fields << public_field
|
24
|
+
self
|
25
|
+
end
|
26
|
+
|
27
|
+
def add_simple_map(model_field, public_field)
|
28
|
+
simple_maps << [model_field, public_field]
|
29
|
+
add_model_field(model_field)
|
30
|
+
add_public_field(public_field)
|
31
|
+
self
|
32
|
+
end
|
33
|
+
|
34
|
+
def add_to_map(model_field, &block)
|
35
|
+
to_maps << [model_field, block]
|
36
|
+
add_model_field(model_field)
|
37
|
+
self
|
38
|
+
end
|
39
|
+
|
40
|
+
def add_from_map(public_field, &block)
|
41
|
+
from_maps << [public_field, block]
|
42
|
+
add_public_field(public_field)
|
43
|
+
self
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|