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.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/.rspec +1 -0
- data/.rubocop.yml +6 -2
- data/.rubocop_todo.yml +38 -9
- data/.travis.yml +29 -3
- data/Appraisals +9 -0
- data/CHANGELOG.md +10 -0
- data/Gemfile +6 -3
- data/README.md +218 -0
- data/Rakefile +3 -1
- data/gemfiles/with_activerecord.gemfile +22 -0
- data/gemfiles/with_mongoid.gemfile +22 -0
- data/grape-roar.gemspec +3 -0
- data/lib/grape-roar.rb +2 -0
- data/lib/grape/roar.rb +3 -0
- data/lib/grape/roar/decorator.rb +2 -0
- data/lib/grape/roar/extensions.rb +3 -0
- data/lib/grape/roar/extensions/relations.rb +23 -0
- data/lib/grape/roar/extensions/relations/adapters.rb +22 -0
- data/lib/grape/roar/extensions/relations/adapters/active_record.rb +35 -0
- data/lib/grape/roar/extensions/relations/adapters/base.rb +49 -0
- data/lib/grape/roar/extensions/relations/adapters/mongoid.rb +38 -0
- data/lib/grape/roar/extensions/relations/dsl_methods.rb +86 -0
- data/lib/grape/roar/extensions/relations/exceptions.rb +14 -0
- data/lib/grape/roar/extensions/relations/mapper.rb +89 -0
- data/lib/grape/roar/extensions/relations/validations.rb +5 -0
- data/lib/grape/roar/extensions/relations/validations/active_record.rb +67 -0
- data/lib/grape/roar/extensions/relations/validations/misc.rb +18 -0
- data/lib/grape/roar/extensions/relations/validations/mongoid.rb +88 -0
- data/lib/grape/roar/formatter.rb +2 -0
- data/lib/grape/roar/representer.rb +2 -0
- data/lib/grape/roar/version.rb +3 -1
- data/spec/config/mongoid.yml +6 -0
- data/spec/decorator_spec.rb +3 -1
- data/spec/extensions/relations/adapters/active_record_spec.rb +26 -0
- data/spec/extensions/relations/adapters/adapters_module_spec.rb +11 -0
- data/spec/extensions/relations/adapters/mongoid_spec.rb +26 -0
- data/spec/extensions/relations/dsl_methods_spec.rb +159 -0
- data/spec/extensions/relations/mapper_spec.rb +88 -0
- data/spec/extensions/relations/validations/active_record_spec.rb +46 -0
- data/spec/extensions/relations/validations/mongoid_spec.rb +88 -0
- data/spec/nested_representer_spec.rb +4 -13
- data/spec/present_with_spec.rb +3 -12
- data/spec/relations_spec.rb +76 -0
- data/spec/representer_spec.rb +3 -12
- data/spec/spec_helper.rb +15 -1
- data/spec/support/{article.rb → all/article.rb} +3 -1
- data/spec/support/{article_representer.rb → all/article_representer.rb} +2 -0
- data/spec/support/all/grape_app_context.rb +18 -0
- data/spec/support/{order.rb → all/order.rb} +3 -1
- data/spec/support/{order_representer.rb → all/order_representer.rb} +3 -1
- data/spec/support/{product.rb → all/product.rb} +2 -0
- data/spec/support/{product_representer.rb → all/product_representer.rb} +3 -1
- data/spec/support/{user.rb → all/user.rb} +2 -0
- data/spec/support/{user_representer.rb → all/user_representer.rb} +2 -0
- data/spec/support/mongoid/relational_models/cart.rb +7 -0
- data/spec/support/mongoid/relational_models/item.rb +7 -0
- data/spec/support/mongoid/relational_models/mongoid_cart_representer.rb +27 -0
- data/spec/support/mongoid/relational_models/mongoid_item_representer.rb +13 -0
- 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: "../"
|
data/grape-roar.gemspec
CHANGED
@@ -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
|
data/lib/grape-roar.rb
CHANGED
data/lib/grape/roar.rb
CHANGED
data/lib/grape/roar/decorator.rb
CHANGED
@@ -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,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
|