grape-roar 0.4.0 → 0.4.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 (61) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.rspec +1 -0
  4. data/.rubocop.yml +6 -2
  5. data/.rubocop_todo.yml +38 -9
  6. data/.travis.yml +29 -3
  7. data/Appraisals +9 -0
  8. data/CHANGELOG.md +10 -0
  9. data/Gemfile +6 -3
  10. data/README.md +218 -0
  11. data/Rakefile +3 -1
  12. data/gemfiles/with_activerecord.gemfile +22 -0
  13. data/gemfiles/with_mongoid.gemfile +22 -0
  14. data/grape-roar.gemspec +3 -0
  15. data/lib/grape-roar.rb +2 -0
  16. data/lib/grape/roar.rb +3 -0
  17. data/lib/grape/roar/decorator.rb +2 -0
  18. data/lib/grape/roar/extensions.rb +3 -0
  19. data/lib/grape/roar/extensions/relations.rb +23 -0
  20. data/lib/grape/roar/extensions/relations/adapters.rb +22 -0
  21. data/lib/grape/roar/extensions/relations/adapters/active_record.rb +35 -0
  22. data/lib/grape/roar/extensions/relations/adapters/base.rb +49 -0
  23. data/lib/grape/roar/extensions/relations/adapters/mongoid.rb +38 -0
  24. data/lib/grape/roar/extensions/relations/dsl_methods.rb +86 -0
  25. data/lib/grape/roar/extensions/relations/exceptions.rb +14 -0
  26. data/lib/grape/roar/extensions/relations/mapper.rb +89 -0
  27. data/lib/grape/roar/extensions/relations/validations.rb +5 -0
  28. data/lib/grape/roar/extensions/relations/validations/active_record.rb +67 -0
  29. data/lib/grape/roar/extensions/relations/validations/misc.rb +18 -0
  30. data/lib/grape/roar/extensions/relations/validations/mongoid.rb +88 -0
  31. data/lib/grape/roar/formatter.rb +2 -0
  32. data/lib/grape/roar/representer.rb +2 -0
  33. data/lib/grape/roar/version.rb +3 -1
  34. data/spec/config/mongoid.yml +6 -0
  35. data/spec/decorator_spec.rb +3 -1
  36. data/spec/extensions/relations/adapters/active_record_spec.rb +26 -0
  37. data/spec/extensions/relations/adapters/adapters_module_spec.rb +11 -0
  38. data/spec/extensions/relations/adapters/mongoid_spec.rb +26 -0
  39. data/spec/extensions/relations/dsl_methods_spec.rb +159 -0
  40. data/spec/extensions/relations/mapper_spec.rb +88 -0
  41. data/spec/extensions/relations/validations/active_record_spec.rb +46 -0
  42. data/spec/extensions/relations/validations/mongoid_spec.rb +88 -0
  43. data/spec/nested_representer_spec.rb +4 -13
  44. data/spec/present_with_spec.rb +3 -12
  45. data/spec/relations_spec.rb +76 -0
  46. data/spec/representer_spec.rb +3 -12
  47. data/spec/spec_helper.rb +15 -1
  48. data/spec/support/{article.rb → all/article.rb} +3 -1
  49. data/spec/support/{article_representer.rb → all/article_representer.rb} +2 -0
  50. data/spec/support/all/grape_app_context.rb +18 -0
  51. data/spec/support/{order.rb → all/order.rb} +3 -1
  52. data/spec/support/{order_representer.rb → all/order_representer.rb} +3 -1
  53. data/spec/support/{product.rb → all/product.rb} +2 -0
  54. data/spec/support/{product_representer.rb → all/product_representer.rb} +3 -1
  55. data/spec/support/{user.rb → all/user.rb} +2 -0
  56. data/spec/support/{user_representer.rb → all/user_representer.rb} +2 -0
  57. data/spec/support/mongoid/relational_models/cart.rb +7 -0
  58. data/spec/support/mongoid/relational_models/item.rb +7 -0
  59. data/spec/support/mongoid/relational_models/mongoid_cart_representer.rb +27 -0
  60. data/spec/support/mongoid/relational_models/mongoid_item_representer.rb +13 -0
  61. metadata +55 -11
@@ -0,0 +1,22 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "mongoid", ">= 5.0.0", require: "mongoid"
6
+
7
+ group :development do
8
+ gem "appraisal", "2.2.0"
9
+ end
10
+
11
+ group :test do
12
+ gem "rack-test"
13
+ gem "rspec", "~> 3.1"
14
+ end
15
+
16
+ group :development, :test do
17
+ gem "nokogiri", "1.6.3.1"
18
+ gem "rake", "~> 10.5.0"
19
+ gem "rubocop", "0.49.1"
20
+ end
21
+
22
+ gemspec path: "../"
@@ -1,4 +1,6 @@
1
1
  # -*- encoding: utf-8 -*-
2
+ # frozen_string_literal: true
3
+
2
4
  require File.expand_path('../lib/grape/roar/version', __FILE__)
3
5
 
4
6
  Gem::Specification.new do |gem|
@@ -17,6 +19,7 @@ Gem::Specification.new do |gem|
17
19
  gem.version = Grape::Roar::VERSION
18
20
 
19
21
  gem.add_dependency 'grape'
22
+ gem.add_dependency 'multi_json'
20
23
  gem.add_dependency 'roar', '~> 1.1.0'
21
24
  gem.required_ruby_version = '>= 2.1.0'
22
25
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'roar'
2
4
  require 'grape'
3
5
  require 'grape/roar'
@@ -1,4 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'grape/roar/version'
2
4
  require 'grape/roar/formatter'
3
5
  require 'grape/roar/representer'
4
6
  require 'grape/roar/decorator'
7
+ require 'grape/roar/extensions'
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'roar/decorator'
2
4
 
3
5
  module Grape
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'grape/roar/extensions/relations'
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'grape/roar/extensions/relations/validations'
4
+ require 'grape/roar/extensions/relations/adapters'
5
+ require 'grape/roar/extensions/relations/dsl_methods'
6
+ require 'grape/roar/extensions/relations/exceptions'
7
+ require 'grape/roar/extensions/relations/mapper'
8
+
9
+ module Grape
10
+ module Roar
11
+ module Extensions
12
+ module Relations
13
+ class << self
14
+ def included(other)
15
+ class << other
16
+ include Extensions::Relations::DSLMethods
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'grape/roar/extensions/relations/adapters/base'
4
+ require 'grape/roar/extensions/relations/adapters/active_record'
5
+ require 'grape/roar/extensions/relations/adapters/mongoid'
6
+
7
+ module Grape
8
+ module Roar
9
+ module Extensions
10
+ module Relations
11
+ module Adapters
12
+ def self.for(klass)
13
+ (constants - [:Base]).inject(nil) do |m, c|
14
+ obj = const_get(c)
15
+ obj.valid_for?(klass) ? obj.new(klass) : m
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ module Roar
5
+ module Extensions
6
+ module Relations
7
+ module Adapters
8
+ class ActiveRecord < Base
9
+ include Validations::ActiveRecord
10
+
11
+ valid_for { |klass| klass < ::ActiveRecord::Base }
12
+
13
+ def collection_methods
14
+ @collection_methods ||= %i[has_many has_and_belongs_to_many]
15
+ end
16
+
17
+ def name_for_represented(represented)
18
+ klass_name = case represented
19
+ when ::ActiveRecord::Relation
20
+ represented.klass.name
21
+ else
22
+ represented.class.name
23
+ end
24
+ klass_name.demodulize.pluralize.downcase
25
+ end
26
+
27
+ def single_entity_methods
28
+ @single_entity_methods ||= %i[has_one belongs_to]
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ module Roar
5
+ module Extensions
6
+ module Relations
7
+ module Adapters
8
+ class Base
9
+ class << self
10
+ def valid_for?(klass)
11
+ valid_proc.call(klass)
12
+ rescue
13
+ false
14
+ end
15
+
16
+ def valid_for(&block)
17
+ @valid_proc = block
18
+ end
19
+
20
+ private
21
+
22
+ attr_reader :valid_proc
23
+ end
24
+
25
+ def initialize(klass)
26
+ @klass = klass
27
+ end
28
+
29
+ def collection_methods
30
+ raise NotImplementedError
31
+ end
32
+
33
+ def name_for_represented(_represented)
34
+ raise NotImplementedError
35
+ end
36
+
37
+ def single_entity_methods
38
+ raise NotImplementedError
39
+ end
40
+
41
+ private
42
+
43
+ attr_reader :klass
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ module Roar
5
+ module Extensions
6
+ module Relations
7
+ module Adapters
8
+ class Mongoid < Base
9
+ include Validations::Mongoid
10
+
11
+ valid_for { |klass| klass < ::Mongoid::Document }
12
+
13
+ def collection_methods
14
+ @collection_methods ||= %i[
15
+ embeds_many has_many has_and_belongs_to_many
16
+ ]
17
+ end
18
+
19
+ def name_for_represented(represented)
20
+ klass_name = if represented.instance_of?(
21
+ ::Mongoid::Relations::Targets::Enumerable
22
+ )
23
+ represented.klass.name
24
+ else
25
+ represented.class.name
26
+ end
27
+ klass_name.demodulize.pluralize.downcase
28
+ end
29
+
30
+ def single_entity_methods
31
+ @single_entity_methods ||= %i[has_one belongs_to embeds_one]
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ module Roar
5
+ module Extensions
6
+ module Relations
7
+ module DSLMethods
8
+ def link_relation(relation, is_collection = false, dsl = self)
9
+ send(is_collection ? :links : :link, relation) do |opts|
10
+ data = represented.send(relation)
11
+
12
+ mapped = Array.wrap(data).map do |object|
13
+ href = [dsl.map_base_url.call(opts),
14
+ dsl.map_resource_path.call(opts, object, relation)].join('/')
15
+
16
+ is_collection ? { href: href } : href
17
+ end
18
+
19
+ is_collection ? mapped : mapped.first
20
+ end
21
+ end
22
+
23
+ def link_self
24
+ relational_mapper[:self] = { relation_kind: :self }
25
+ end
26
+
27
+ def map_base_url(&block)
28
+ @map_base_url ||= if block.nil?
29
+ proc do |opts|
30
+ request = Grape::Request.new(opts[:env])
31
+ "#{request.base_url}#{request.script_name}"
32
+ end
33
+ else
34
+ block
35
+ end
36
+ end
37
+
38
+ def map_self_url(dsl = self)
39
+ link :self do |opts|
40
+ resource_path = dsl.name_for_represented(represented)
41
+ [dsl.map_base_url.call(opts),
42
+ "#{resource_path}/#{represented.try(:id)}"].join('/')
43
+ end
44
+ end
45
+
46
+ def map_resource_path(&block)
47
+ @map_resource_path ||= if block.nil?
48
+ proc do |_opts, object, relation_name|
49
+ "#{relation_name}/#{object.id}"
50
+ end
51
+ else
52
+ block
53
+ end
54
+ end
55
+
56
+ def name_for_represented(represented)
57
+ relational_mapper.adapter.name_for_represented(represented)
58
+ end
59
+
60
+ def relation(relation_kind, rname, opts = {})
61
+ relational_mapper[rname] = opts.merge(relation_kind: relation_kind)
62
+ end
63
+
64
+ def represent(object, _options)
65
+ object.extend(self) unless is_a?(Class)
66
+ map_relations(object) unless relations_mapped
67
+ is_a?(Class) ? super : object
68
+ end
69
+
70
+ private
71
+
72
+ attr_reader :relations_mapped
73
+
74
+ def map_relations(object)
75
+ relational_mapper.decorate(object.class)
76
+ @relations_mapped = true
77
+ end
78
+
79
+ def relational_mapper
80
+ @relational_mapper ||= Mapper.new(self)
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ module Roar
5
+ module Extensions
6
+ module Relations
7
+ module Exceptions
8
+ class InvalidRelationError < StandardError; end
9
+ class UnsupportedRelationError < StandardError; end
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ module Roar
5
+ module Extensions
6
+ module Relations
7
+ class Mapper
8
+ extend Forwardable
9
+
10
+ def initialize(entity)
11
+ @entity = entity
12
+ @config = {}
13
+ end
14
+
15
+ def adapter
16
+ @adapter ||= Adapters.for(model_klass)
17
+ end
18
+
19
+ def decorate(klass)
20
+ @model_klass = klass
21
+
22
+ config.each_pair do |relation, opts|
23
+ representer_for(relation.to_s, opts) unless opts.key(:extend)
24
+ map_relation(relation, opts)
25
+ end
26
+ end
27
+
28
+ def_delegators :@config, :[], :[]=
29
+
30
+ private
31
+
32
+ attr_reader :config, :entity, :model_klass
33
+
34
+ def find_representer(base, target_name, const)
35
+ const = base.const_get(const)
36
+ return false if const.nil? || !const.is_a?(Module)
37
+ (const < ::Roar::JSON::HAL) && const.name
38
+ .downcase
39
+ .include?(target_name.singularize)
40
+ end
41
+
42
+ def map_collection(relation, opts)
43
+ return entity.link_relation(relation, true) unless opts.fetch(:embedded, false)
44
+ entity.collection(relation, opts)
45
+ end
46
+
47
+ def map_relation(relation, opts)
48
+ map = if adapter.collection_methods.include?(opts[:relation_kind])
49
+ :map_collection
50
+ elsif adapter.single_entity_methods
51
+ .include?(opts[:relation_kind]) || opts[:relation_kind] == :self
52
+ :map_single_entity
53
+ else raise Exceptions::UnsupportedRelationError,
54
+ 'No such relation supported'
55
+ end
56
+
57
+ send(map, relation, opts)
58
+ validate_relation(relation, opts[:relation_kind])
59
+ end
60
+
61
+ def map_single_entity(relation, opts)
62
+ return entity.map_self_url if opts[:relation_kind] == :self
63
+ return entity.link_relation(relation) unless opts.fetch(:embedded, false)
64
+ entity.property(relation, opts)
65
+ end
66
+
67
+ def representer_for(relation, opts)
68
+ base_path = entity.name.deconstantize
69
+ base_path = base_path.empty? ? Object : base_path.safe_constantize
70
+ return if base_path.nil?
71
+
72
+ to_extend = base_path.constants
73
+ .find(&method(:find_representer).curry[
74
+ base_path, relation
75
+ ])
76
+
77
+ opts.merge!(extend: "#{base_path}::#{to_extend}".safe_constantize)
78
+ end
79
+
80
+ def validate_relation(relation, kind)
81
+ validator_method = "#{kind}_valid?"
82
+ return true unless adapter.respond_to?(validator_method)
83
+ adapter.send(validator_method, relation.to_s)
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'grape/roar/extensions/relations/validations/misc'
4
+ require 'grape/roar/extensions/relations/validations/active_record'
5
+ require 'grape/roar/extensions/relations/validations/mongoid'
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ module Roar
5
+ module Extensions
6
+ module Relations
7
+ module Validations
8
+ module ActiveRecord
9
+ include Validations::Misc
10
+
11
+ def belongs_to_valid?(relation)
12
+ relation = klass.reflections[relation]
13
+
14
+ return true if relation.is_a?(
15
+ ::ActiveRecord::Reflection::BelongsToReflection
16
+ )
17
+
18
+ invalid_relation(
19
+ ::ActiveRecord::Reflection::BelongsToReflection,
20
+ relation.class
21
+ )
22
+ end
23
+
24
+ def has_many_valid?(relation)
25
+ relation = klass.reflections[relation]
26
+
27
+ return true if relation.is_a?(
28
+ ::ActiveRecord::Reflection::HasManyReflection
29
+ )
30
+
31
+ invalid_relation(
32
+ ::ActiveRecord::Reflection::HasManyReflection,
33
+ relation.class
34
+ )
35
+ end
36
+
37
+ def has_and_belongs_to_many_valid?(relation)
38
+ relation = klass.reflections[relation]
39
+
40
+ return true if relation.is_a?(
41
+ ::ActiveRecord::Reflection::HasAndBelongsToManyReflection
42
+ )
43
+
44
+ invalid_relation(
45
+ ::ActiveRecord::Reflection::HasAndBelongsToManyReflection,
46
+ relation.class
47
+ )
48
+ end
49
+
50
+ def has_one_valid?(relation)
51
+ relation = klass.reflections[relation]
52
+
53
+ return true if relation.is_a?(
54
+ ::ActiveRecord::Reflection::HasOneReflection
55
+ )
56
+
57
+ invalid_relation(
58
+ ::ActiveRecord::Reflection::HasOneReflection,
59
+ relation.class
60
+ )
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end