yaks 0.9.0 → 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (120) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +158 -56
  3. data/Rakefile +1 -3
  4. data/ataru_setup.rb +72 -0
  5. data/find_missing_tests.rb +34 -0
  6. data/lib/yaks.rb +8 -10
  7. data/lib/yaks/breaking_changes.rb +4 -6
  8. data/lib/yaks/builder.rb +1 -1
  9. data/lib/yaks/changelog.rb +1 -1
  10. data/lib/yaks/collection_mapper.rb +8 -5
  11. data/lib/yaks/collection_resource.rb +0 -1
  12. data/lib/yaks/config.rb +17 -7
  13. data/lib/yaks/configurable.rb +20 -14
  14. data/lib/yaks/default_policy.rb +82 -33
  15. data/lib/yaks/format.rb +7 -3
  16. data/lib/yaks/format/collection_json.rb +4 -4
  17. data/lib/yaks/format/hal.rb +1 -2
  18. data/lib/yaks/format/halo.rb +2 -4
  19. data/lib/yaks/format/json_api.rb +46 -27
  20. data/lib/yaks/html5_forms.rb +0 -2
  21. data/lib/yaks/mapper.rb +5 -5
  22. data/lib/yaks/mapper/association.rb +7 -7
  23. data/lib/yaks/mapper/association_mapper.rb +2 -0
  24. data/lib/yaks/mapper/attribute.rb +10 -4
  25. data/lib/yaks/mapper/config.rb +2 -2
  26. data/lib/yaks/mapper/form.rb +4 -10
  27. data/lib/yaks/mapper/form/config.rb +16 -17
  28. data/lib/yaks/mapper/form/dynamic_field.rb +1 -1
  29. data/lib/yaks/mapper/form/field.rb +16 -7
  30. data/lib/yaks/mapper/form/field/option.rb +5 -4
  31. data/lib/yaks/mapper/form/fieldset.rb +1 -1
  32. data/lib/yaks/mapper/form/legend.rb +18 -0
  33. data/lib/yaks/mapper/has_many.rb +1 -0
  34. data/lib/yaks/mapper/link.rb +7 -4
  35. data/lib/yaks/null_resource.rb +4 -5
  36. data/lib/yaks/pipeline.rb +2 -2
  37. data/lib/yaks/primitivize.rb +3 -2
  38. data/lib/yaks/reader/hal.rb +12 -13
  39. data/lib/yaks/reader/json_api.rb +50 -33
  40. data/lib/yaks/resource.rb +6 -7
  41. data/lib/yaks/resource/form.rb +2 -12
  42. data/lib/yaks/resource/form/field.rb +4 -3
  43. data/lib/yaks/resource/form/field/option.rb +1 -1
  44. data/lib/yaks/resource/form/fieldset.rb +1 -1
  45. data/lib/yaks/resource/form/legend.rb +18 -0
  46. data/lib/yaks/resource/has_fields.rb +13 -7
  47. data/lib/yaks/resource/link.rb +1 -1
  48. data/lib/yaks/runner.rb +5 -2
  49. data/lib/yaks/serializer.rb +2 -3
  50. data/lib/yaks/util.rb +7 -8
  51. data/lib/yaks/version.rb +1 -1
  52. data/spec/acceptance/acceptance_spec.rb +53 -38
  53. data/spec/acceptance/json_shared_examples.rb +45 -12
  54. data/spec/acceptance/models.rb +1 -1
  55. data/spec/integration/dynamic_form_fields_spec.rb +0 -1
  56. data/spec/integration/fieldset_spec.rb +18 -20
  57. data/spec/integration/map_to_resource_spec.rb +6 -6
  58. data/spec/json/{confucius.collection.json → confucius.collection_json.json} +0 -0
  59. data/spec/json/confucius.json_api.json +43 -27
  60. data/spec/json/list_of_quotes.collection_json.json +43 -0
  61. data/spec/json/list_of_quotes.hal.json +18 -0
  62. data/spec/json/list_of_quotes.json_api.json +25 -0
  63. data/spec/json/youtypeitwepostit.collection_json.json +45 -0
  64. data/spec/spec_helper.rb +4 -3
  65. data/spec/support/classes_for_policy_testing.rb +38 -14
  66. data/spec/support/deep_eql.rb +21 -18
  67. data/spec/support/pet_mapper.rb +2 -0
  68. data/spec/support/shared_contexts.rb +9 -9
  69. data/spec/unit/yaks/builder_spec.rb +41 -18
  70. data/spec/unit/yaks/collection_mapper_spec.rb +22 -19
  71. data/spec/unit/yaks/collection_resource_spec.rb +16 -8
  72. data/spec/unit/yaks/config_spec.rb +215 -19
  73. data/spec/unit/yaks/configurable_spec.rb +66 -7
  74. data/spec/unit/yaks/default_policy/derive_mapper_from_collection_spec.rb +47 -0
  75. data/spec/unit/yaks/default_policy/derive_mapper_from_item_spec.rb +114 -0
  76. data/spec/unit/yaks/default_policy/derive_mapper_from_object_spec.rb +29 -71
  77. data/spec/unit/yaks/default_policy_spec.rb +4 -5
  78. data/spec/unit/yaks/format/collection_json_spec.rb +35 -36
  79. data/spec/unit/yaks/format/hal_spec.rb +3 -3
  80. data/spec/unit/yaks/format/json_api_spec.rb +109 -68
  81. data/spec/unit/yaks/format_spec.rb +34 -0
  82. data/spec/unit/yaks/fp/callable_spec.rb +5 -3
  83. data/spec/unit/yaks/mapper/association_mapper_spec.rb +22 -4
  84. data/spec/unit/yaks/mapper/association_spec.rb +23 -11
  85. data/spec/unit/yaks/mapper/attribute_spec.rb +46 -7
  86. data/spec/unit/yaks/mapper/config_spec.rb +2 -3
  87. data/spec/unit/yaks/mapper/form/config_spec.rb +95 -0
  88. data/spec/unit/yaks/mapper/form/dynamic_field_spec.rb +30 -0
  89. data/spec/unit/yaks/mapper/form/field/option_spec.rb +48 -4
  90. data/spec/unit/yaks/mapper/form/field_spec.rb +43 -2
  91. data/spec/unit/yaks/mapper/form/fieldset_spec.rb +67 -8
  92. data/spec/unit/yaks/mapper/form/legend_spec.rb +52 -0
  93. data/spec/unit/yaks/mapper/form_spec.rb +84 -23
  94. data/spec/unit/yaks/mapper/has_many_spec.rb +39 -36
  95. data/spec/unit/yaks/mapper/has_one_spec.rb +28 -20
  96. data/spec/unit/yaks/mapper/link_spec.rb +68 -16
  97. data/spec/unit/yaks/mapper_spec.rb +118 -30
  98. data/spec/unit/yaks/null_resource_spec.rb +83 -52
  99. data/spec/unit/yaks/pipeline_spec.rb +101 -74
  100. data/spec/unit/yaks/primitivize_spec.rb +25 -6
  101. data/spec/unit/yaks/resource/form/field_spec.rb +5 -5
  102. data/spec/unit/yaks/resource/form/fieldset_spec.rb +7 -0
  103. data/spec/unit/yaks/resource/form/legend_spec.rb +8 -0
  104. data/spec/unit/yaks/resource/form_spec.rb +17 -37
  105. data/spec/unit/yaks/resource/has_fields_spec.rb +44 -3
  106. data/spec/unit/yaks/resource/link_spec.rb +11 -6
  107. data/spec/unit/yaks/resource_spec.rb +87 -98
  108. data/spec/unit/yaks/runner_spec.rb +112 -28
  109. data/spec/unit/yaks/serializer_spec.rb +1 -1
  110. data/spec/unit/yaks/util_spec.rb +30 -10
  111. data/spec/yaml/list_of_quotes.yaml +13 -0
  112. data/yaks.gemspec +21 -13
  113. metadata +129 -41
  114. data/lib/yaks/attributes.rb +0 -86
  115. data/lib/yaks/fp.rb +0 -26
  116. data/lib/yaks/identifier/link_relation.rb +0 -17
  117. data/resources/iana-link-relations.csv +0 -152
  118. data/spec/json/youtypeitwepostit.collection.json +0 -45
  119. data/spec/unit/yaks/attributes_spec.rb +0 -178
  120. data/spec/unit/yaks/fp_spec.rb +0 -29
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'mutant'
4
+ require 'pry'
5
+
6
+ # These are private methods that are tested by other methods in the same class
7
+ SKIP = %w[
8
+ Yaks::CollectionMapper#collection_rel
9
+ Yaks::CollectionMapper#collection_type
10
+ Yaks::CollectionMapper#mapper_for_model
11
+ Yaks::Resource::Form::Field#select_options_for_value
12
+ Yaks::Mapper::AssociationMapper#add_link
13
+ Yaks::Mapper::AssociationMapper#add_subresource
14
+ Yaks::Mapper::Link#resource_link_options
15
+ ]
16
+
17
+ args = ["-Ilib", "-ryaks", "--use", "rspec", "Yaks*"]
18
+ env = Mutant::Env::Bootstrap.call(Mutant::CLI.call(args))
19
+
20
+ integration = env.config.integration
21
+
22
+ integration.setup
23
+ binding.pry if integration.all_tests.empty? # rubocop:disable Lint/Debugger
24
+
25
+ env.subjects.each do |subject|
26
+ match_expression = subject.match_expressions.first
27
+ subject_tests = integration.all_tests.select do |test|
28
+ match_expression.prefix?(test.expression)
29
+ end
30
+ unless subject_tests.any? || SKIP.include?(subject.expression.syntax)
31
+ puts subject.identification
32
+ exit if ARGV.include?("-1")
33
+ end
34
+ end
data/lib/yaks.rb CHANGED
@@ -4,17 +4,15 @@ require 'pathname'
4
4
  require 'json'
5
5
  require 'csv'
6
6
 
7
- require 'anima'
8
7
  require 'concord'
8
+ require 'attribs'
9
9
  require 'inflection'
10
10
  require 'uri_template'
11
11
  require 'rack/accept'
12
12
 
13
13
  require 'yaks/version'
14
14
  require 'yaks/util'
15
- require 'yaks/attributes'
16
15
  require 'yaks/configurable'
17
- require 'yaks/fp'
18
16
  require 'yaks/fp/callable'
19
17
  require 'yaks/primitivize'
20
18
  require 'yaks/builder'
@@ -24,7 +22,6 @@ require 'yaks/default_policy'
24
22
  require 'yaks/serializer'
25
23
  require 'yaks/config'
26
24
 
27
-
28
25
  module Yaks
29
26
  Undefined = Module.new.freeze
30
27
 
@@ -34,6 +31,7 @@ module Yaks
34
31
  DSL_METHODS = [
35
32
  :format_options,
36
33
  :rel_template,
34
+ :mapper_for,
37
35
  :before,
38
36
  :after,
39
37
  :around,
@@ -42,13 +40,13 @@ module Yaks
42
40
  :mapper_namespace,
43
41
  :serializer,
44
42
  :json_serializer,
45
- :map_to_primitive,
43
+ :map_to_primitive
46
44
  ]
47
45
 
48
46
  ConfigBuilder = Builder.new(Yaks::Config) do
49
- def_set *Yaks::Config.attributes.names
50
- def_forward *DSL_METHODS
51
- def_forward *Yaks::DefaultPolicy.public_instance_methods(false)
47
+ def_set(*Yaks::Config.attributes.names)
48
+ def_forward(*DSL_METHODS)
49
+ def_forward(*Yaks::DefaultPolicy.public_instance_methods(false))
52
50
  end
53
51
 
54
52
  class << self
@@ -61,14 +59,12 @@ module Yaks
61
59
  end
62
60
  end
63
61
 
64
-
65
62
  require 'yaks/resource'
66
63
  require 'yaks/null_resource'
67
64
  require 'yaks/resource/link'
68
65
  require 'yaks/collection_resource'
69
66
 
70
67
  require 'yaks/html5_forms'
71
- require 'yaks/identifier/link_relation'
72
68
 
73
69
  require 'yaks/mapper/association'
74
70
  require 'yaks/mapper/has_one'
@@ -78,6 +74,7 @@ require 'yaks/mapper/link'
78
74
  require 'yaks/mapper/form/field/option'
79
75
  require 'yaks/mapper/form/field'
80
76
  require 'yaks/mapper/form/fieldset'
77
+ require 'yaks/mapper/form/legend'
81
78
  require 'yaks/mapper/form/dynamic_field'
82
79
  require 'yaks/mapper/form/config'
83
80
  require 'yaks/mapper/form'
@@ -91,6 +88,7 @@ require 'yaks/resource/form'
91
88
  require 'yaks/resource/form/field'
92
89
  require 'yaks/resource/form/field/option'
93
90
  require 'yaks/resource/form/fieldset'
91
+ require 'yaks/resource/form/legend'
94
92
 
95
93
  require 'yaks/format'
96
94
  require 'yaks/format/hal'
@@ -1,11 +1,10 @@
1
1
  module Yaks
2
-
3
2
  # These are displayed in a post-install message when installing the
4
3
  # gem to aid upgraiding
5
4
 
6
5
  BreakingChanges = {
7
6
 
8
- '0.7.6' => %q~
7
+ '0.7.6' => %q~
9
8
  Breaking Changes in Yaks 0.7.6
10
9
  ==============================
11
10
  Breaking change: using a symbol instead of link template no longer
@@ -27,7 +26,7 @@ with the attribute name in HTML. An alias is available but will output
27
26
  a deprecation warning.
28
27
  ~,
29
28
 
30
- '0.7.0' => %q~
29
+ '0.7.0' => %q~
31
30
  Breaking Changes in Yaks 0.7.0
32
31
  ==============================
33
32
  Yaks::Resource#subresources is now an array, not a hash. The rel is
@@ -40,7 +39,7 @@ have custom implementations of any of these, or hooks that are not
40
39
  specified as ruby blocks, you will need to take this into account
41
40
  ~,
42
41
 
43
- '0.5.0' => %q~
42
+ '0.5.0' => %q~
44
43
 
45
44
  Breaking Changes in Yaks 0.5.0
46
45
  ==============================
@@ -61,7 +60,7 @@ for full documentation.
61
60
 
62
61
  ~,
63
62
 
64
- '0.4.3' => %q~
63
+ '0.4.3' => %q~
65
64
 
66
65
  Breaking Changes in Yaks 0.4.3
67
66
  ==============================
@@ -81,5 +80,4 @@ documentation.
81
80
 
82
81
  BreakingChanges['0.4.4'] = BreakingChanges['0.4.3']
83
82
  BreakingChanges['0.7.1'] = BreakingChanges['0.7.0']
84
-
85
83
  end
data/lib/yaks/builder.rb CHANGED
@@ -22,7 +22,7 @@ module Yaks
22
22
  def initialize(klass, methods = [], &block)
23
23
  @klass = klass
24
24
  @methods = methods
25
- def_forward *methods if methods.any?
25
+ def_forward(*methods) if methods.any?
26
26
  instance_eval(&block) if block
27
27
  end
28
28
 
@@ -1,6 +1,6 @@
1
1
  module Yaks
2
2
  module Changelog
3
- extend self
3
+ module_function
4
4
 
5
5
  def current
6
6
  versions[Yaks::VERSION]
@@ -1,6 +1,6 @@
1
1
  module Yaks
2
2
  class CollectionMapper < Mapper
3
- alias collection object
3
+ alias_method :collection, :object
4
4
 
5
5
  # @param [Array] collection
6
6
  # @return [Array]
@@ -14,9 +14,12 @@ module Yaks
14
14
  end
15
15
  }
16
16
 
17
- if context[:mapper_stack].empty?
18
- attrs[:rels] = [collection_rel]
19
- end
17
+ # For collections from associations the rel will be based on the
18
+ # association. At the top level there's no association, so we
19
+ # use a generic rel. This matters especially for HAL, where a
20
+ # top-level collection is rendered as an object with the
21
+ # collection as a subresource.
22
+ attrs[:rels] = [collection_rel] if context[:mapper_stack].empty?
20
23
 
21
24
  map_attributes(
22
25
  map_links(
@@ -29,7 +32,7 @@ module Yaks
29
32
 
30
33
  def collection_rel
31
34
  if collection_type
32
- policy.expand_rel( pluralize( collection_type ) )
35
+ policy.expand_rel(pluralize(collection_type))
33
36
  else
34
37
  'collection'
35
38
  end
@@ -19,6 +19,5 @@ module Yaks
19
19
  def seq
20
20
  self
21
21
  end
22
-
23
22
  end
24
23
  end
data/lib/yaks/config.rb CHANGED
@@ -2,7 +2,7 @@ module Yaks
2
2
  class Config
3
3
  extend Yaks::Util::Deprecated
4
4
  include Yaks::FP::Callable,
5
- Attributes.new(
5
+ Attribs.new(
6
6
  format_options_hash: Hash.new({}),
7
7
  default_format: :hal,
8
8
  policy_options: {},
@@ -13,12 +13,12 @@ module Yaks
13
13
  )
14
14
 
15
15
  class << self
16
- alias create new
16
+ alias_method :create, :new
17
17
  end
18
18
 
19
19
  deprecated_alias :namespace, :mapper_namespace
20
20
 
21
- def format_options(format, options = Undefined)
21
+ def format_options(format, options)
22
22
  with(format_options_hash: format_options_hash.merge(format => options))
23
23
  end
24
24
 
@@ -27,7 +27,7 @@ module Yaks
27
27
  end
28
28
 
29
29
  def json_serializer(&serializer)
30
- with(serializer: :json, &serializer)
30
+ serializer(:json, &serializer)
31
31
  end
32
32
 
33
33
  %w[before after around skip].map(&:intern).each do |hook_type|
@@ -37,11 +37,17 @@ module Yaks
37
37
  end
38
38
 
39
39
  def rel_template(template)
40
- with(policy_options: policy_options.merge(:rel_template => template))
40
+ with(policy_options: policy_options.merge(rel_template: template))
41
41
  end
42
42
 
43
43
  def mapper_namespace(namespace)
44
- with(policy_options: policy_options.merge(:namespace => namespace))
44
+ with(policy_options: policy_options.merge(namespace: namespace))
45
+ end
46
+
47
+ def mapper_for(rule, mapper_class)
48
+ policy_options[:mapper_rules] ||= {}
49
+ mapper_rules = policy_options[:mapper_rules].merge(rule => mapper_class)
50
+ with(policy_options: policy_options.merge(mapper_rules: mapper_rules))
45
51
  end
46
52
 
47
53
  def map_to_primitive(*args, &block)
@@ -80,12 +86,16 @@ module Yaks
80
86
  def call(object, options = {})
81
87
  runner(object, options).call
82
88
  end
83
- alias serialize call
89
+ alias_method :serialize, :call
84
90
 
85
91
  def map(object, options = {})
86
92
  runner(object, options).map
87
93
  end
88
94
 
95
+ def format(data, options = {})
96
+ runner(data, options).format
97
+ end
98
+
89
99
  def read(data, options = {})
90
100
  runner(data, options).read
91
101
  end
@@ -34,12 +34,16 @@ module Yaks
34
34
  def def_set(*method_names)
35
35
  method_names.each do |method_name|
36
36
  define_singleton_method method_name do |arg = Undefined, &block|
37
- if arg == Undefined && block
38
- self.config = config.update(method_name => block)
39
- elsif arg == Undefined
40
- raise ArgumentError, "wrong number of arguments (0 for 1)"
37
+ if arg.equal?(Undefined)
38
+ unless block
39
+ raise ArgumentError, "setting #{method_name}: no value and no block given"
40
+ end
41
+ self.config = config.with(method_name => block)
41
42
  else
42
- self.config = config.update(method_name => arg)
43
+ if block
44
+ raise ArgumentError, "ambiguous invocation setting #{method_name}: give either a value or a block, not both."
45
+ end
46
+ self.config = config.with(method_name => arg)
43
47
  end
44
48
  end
45
49
  end
@@ -50,15 +54,15 @@ module Yaks
50
54
  #
51
55
  # Either takes a list of methods to forward, or a mapping (hash)
52
56
  # of source to destination method name.
53
- def def_forward(method_names, *args)
54
- unless method_names.is_a? Hash
55
- def_forward([method_names, *args].map{|name| {name => name}}.inject(:merge))
56
- return
57
- end
58
- method_names.each do |method_name, target|
59
- define_singleton_method method_name do |*args, &block|
60
- self.config = config.public_send(target, *args, &block)
57
+ def def_forward(mappings, *names)
58
+ if mappings.instance_of? Hash
59
+ mappings.each do |method_name, target|
60
+ define_singleton_method method_name do |*args, &block|
61
+ self.config = config.public_send(target, *args, &block)
62
+ end
61
63
  end
64
+ else
65
+ def_forward([mappings, *names].map{|name| {name => name}}.inject(:merge))
62
66
  end
63
67
  end
64
68
 
@@ -70,6 +74,7 @@ module Yaks
70
74
  # This will generate a `fieldset` method, which will call
71
75
  # `Fieldset.create`, and append the result to `config.fields`
72
76
  def def_add(name, options)
77
+ old_verbose, $VERBOSE = $VERBOSE, false # skip method redefinition warning
73
78
  define_singleton_method name do |*args, &block|
74
79
  defaults = options.fetch(:defaults, {})
75
80
  klass = options.fetch(:create)
@@ -85,7 +90,8 @@ module Yaks
85
90
  klass.create(*args, &block)
86
91
  )
87
92
  end
93
+ ensure
94
+ $VERBOSE = old_verbose
88
95
  end
89
-
90
96
  end
91
97
  end
@@ -1,3 +1,4 @@
1
+
1
2
  module Yaks
2
3
  class DefaultPolicy
3
4
  include Util
@@ -5,7 +6,8 @@ module Yaks
5
6
  # Default policy options.
6
7
  DEFAULTS = {
7
8
  rel_template: "rel:{rel}",
8
- namespace: Kernel
9
+ namespace: Object,
10
+ mapper_rules: {}
9
11
  }
10
12
 
11
13
  # @!attribute [r]
@@ -17,36 +19,57 @@ module Yaks
17
19
  @options = DEFAULTS.merge(options)
18
20
  end
19
21
 
22
+ # Main point of entry for mapper derivation. Calls
23
+ # derive_mapper_from_collection or derive_mapper_from_item
24
+ # depending on the model.
25
+ #
20
26
  # @param model [Object]
21
27
  # @return [Class] A mapper, typically a subclass of Yaks::Mapper
22
28
  #
23
- # @raise [NameError] only occurs when the model is anything but a collection.
29
+ # @raise [RuntimeError] occurs when no mapper is found
24
30
  def derive_mapper_from_object(model)
25
- if model.respond_to? :to_ary
26
- if m = model.first
27
- name = m.class.name.split('::').last + 'CollectionMapper'
28
- begin
29
- return @options[:namespace].const_get(name)
30
- rescue NameError
31
- end
32
- end
33
- begin
34
- return @options[:namespace].const_get(:CollectionMapper)
35
- rescue NameError
36
- end
37
- CollectionMapper
38
- else
39
- klass = model.class
31
+ mapper = detect_configured_mapper_for(model)
32
+ return mapper if mapper
33
+ return derive_mapper_from_collection(model) if model.respond_to? :to_ary
34
+ derive_mapper_from_item(model)
35
+ end
36
+
37
+ # Derives a mapper from the given collection.
38
+ #
39
+ # @param collection [Object]
40
+ # @return [Class] A mapper, typically a subclass of Yaks::Mapper
41
+ def derive_mapper_from_collection(collection)
42
+ if m = collection.first
43
+ name = "#{m.class.name.split('::').last}CollectionMapper"
40
44
  begin
41
- name = klass.name.split('::').last
42
- return @options[:namespace].const_get(name + 'Mapper')
43
- rescue NameError
44
- klass = klass.superclass
45
- retry if klass
45
+ return @options[:namespace].const_get(name)
46
+ rescue NameError # rubocop:disable Lint/HandleExceptions
46
47
  end
47
- name = model.class.name.split('::').last
48
- raise "Failed to find a mapper for #{model.inspect}. Did you mean to implement #{name}Mapper?"
49
48
  end
49
+ begin
50
+ return @options[:namespace].const_get(:CollectionMapper)
51
+ rescue NameError # rubocop:disable Lint/HandleExceptions
52
+ end
53
+ CollectionMapper
54
+ end
55
+
56
+ # Derives a mapper from the given item. This item should not
57
+ # be a collection.
58
+ #
59
+ # @param item [Object]
60
+ # @return [Class] A mapper, typically a subclass of Yaks::Mapper
61
+ #
62
+ # @raise [RuntimeError] only occurs when no mapper is found for the given item.
63
+ def derive_mapper_from_item(item)
64
+ klass = item.class
65
+ namespaces = klass.name.split("::")[0...-1]
66
+ begin
67
+ return build_mapper_class(namespaces, klass)
68
+ rescue NameError
69
+ klass = next_class_for_lookup(item, namespaces, klass)
70
+ retry if klass
71
+ end
72
+ raise_mapper_not_found(item)
50
73
  end
51
74
 
52
75
  # Derive the a mapper type name
@@ -63,9 +86,9 @@ module Yaks
63
86
 
64
87
  # Derive the mapper type name from a collection
65
88
  #
66
- # This inspects the first element of the collection, so it
67
- # requires a non-empty collection. Will return nil if the
68
- # collection is empty.
89
+ # This inspects the first element of the collection, so
90
+ # it requires a collection with truthy elements. Will
91
+ # return `nil` if the collection has no truthy elements.
69
92
  #
70
93
  # @param [#first] collection
71
94
  #
@@ -73,11 +96,8 @@ module Yaks
73
96
  #
74
97
  # @raise [NameError]
75
98
  def derive_type_from_collection(collection)
76
- if collection.any?
77
- derive_type_from_mapper_class(
78
- derive_mapper_from_object(collection.first)
79
- )
80
- end
99
+ return if collection.none?
100
+ derive_type_from_mapper_class(derive_mapper_from_object(collection.first))
81
101
  end
82
102
 
83
103
  def derive_mapper_from_association(association)
@@ -87,7 +107,7 @@ module Yaks
87
107
  # @param association [Yaks::Mapper::Association]
88
108
  # @return [String]
89
109
  def derive_rel_from_association(association)
90
- expand_rel( association.name )
110
+ expand_rel(association.name)
91
111
  end
92
112
 
93
113
  # @param relname [String]
@@ -96,5 +116,34 @@ module Yaks
96
116
  URITemplate.new(@options[:rel_template]).expand(rel: relname)
97
117
  end
98
118
 
119
+ private
120
+
121
+ def build_mapper_class(namespaces, klass)
122
+ mapper_class = "#{klass.name.split('::').last}Mapper"
123
+ [*namespaces, mapper_class].inject(@options[:namespace]) do |namespace, module_or_class|
124
+ namespace.const_get(module_or_class, false)
125
+ end
126
+ end
127
+
128
+ def next_class_for_lookup(item, namespaces, klass)
129
+ superclass = klass.superclass
130
+ return superclass if superclass < Object
131
+ return nil if namespaces.empty?
132
+ namespaces.clear
133
+ item.class
134
+ end
135
+
136
+ def raise_mapper_not_found(item)
137
+ namespace = "#{@options[:namespace]}::" unless Object.equal?(@options[:namespace])
138
+ mapper_class = "#{namespace}#{item.class}Mapper"
139
+ raise "Failed to find a mapper for #{item.inspect}. Did you mean to implement #{mapper_class}?"
140
+ end
141
+
142
+ def detect_configured_mapper_for(object)
143
+ @options[:mapper_rules].each do |rule, mapper_class|
144
+ return mapper_class if rule === object # rubocop:disable Style/CaseEquality
145
+ end
146
+ nil
147
+ end
99
148
  end
100
149
  end