rails_view_adapters 0.2.1

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.
Files changed (35) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +10 -0
  3. data/.rspec +2 -0
  4. data/.rubocop.yml +181 -0
  5. data/.travis.yml +7 -0
  6. data/Gemfile +4 -0
  7. data/LICENSE.md +27 -0
  8. data/README.md +88 -0
  9. data/Rakefile +6 -0
  10. data/lib/rails_view_adapters/adapter.rb +34 -0
  11. data/lib/rails_view_adapters/adapter_base.rb +101 -0
  12. data/lib/rails_view_adapters/definition_proxy.rb +155 -0
  13. data/lib/rails_view_adapters/map.rb +46 -0
  14. data/lib/rails_view_adapters/version.rb +4 -0
  15. data/lib/rails_view_adapters.rb +13 -0
  16. data/rails_view_adapters.gemspec +35 -0
  17. data/spec/active_record_helper.rb +16 -0
  18. data/spec/active_record_support/db/migrate/20161031222934_create_users.rb +14 -0
  19. data/spec/active_record_support/db/migrate/20161031223219_create_posts.rb +11 -0
  20. data/spec/active_record_support/db/migrate/20161031223253_create_teams.rb +10 -0
  21. data/spec/active_record_support/fabricators/post_fabricator.rb +7 -0
  22. data/spec/active_record_support/fabricators/team_fabricator.rb +6 -0
  23. data/spec/active_record_support/fabricators/user_fabricator.rb +10 -0
  24. data/spec/active_record_support/models/post.rb +4 -0
  25. data/spec/active_record_support/models/team.rb +4 -0
  26. data/spec/active_record_support/models/user.rb +5 -0
  27. data/spec/integration/an_adapter_spec.rb +58 -0
  28. data/spec/lib/adapter_base_spec.rb +133 -0
  29. data/spec/lib/adapter_spec.rb +45 -0
  30. data/spec/lib/definition_proxy_spec.rb +242 -0
  31. data/spec/lib/map_spec.rb +99 -0
  32. data/spec/spec_helper.rb +19 -0
  33. data/spec/support/an_adapter.rb +72 -0
  34. data/spec/support/fuzzy_nested_match.rb +96 -0
  35. 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
@@ -0,0 +1,10 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ /.idea/
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
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
@@ -0,0 +1,7 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.3.1
5
+ - 2.2.1
6
+
7
+ before_install: gem install bundler -v 1.13.6
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in rails_view_adapters.gemspec
4
+ gemspec
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,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -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
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+ module RailsViewAdapters
3
+ VERSION = "0.2.1"
4
+ end