grape-roar 0.4.0 → 0.4.1

Sign up to get free protection for your applications and to get access to all the features.
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