-
Notifications
You must be signed in to change notification settings - Fork 0
/
included_resource_params.rb
154 lines (139 loc) · 5.57 KB
/
included_resource_params.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
##
# The IncludedResourceParams class is responsible for parsing a string containing
# a comma separated list of associated resources to include with a request. See
# http://jsonapi.org/format/#fetching-includes for additional details although
# this is not required knowledge for the task at hand.
#
# Our API requires specific inclusion of related resourses - that is we do NOT
# want to support wildcard inclusion (e.g. `foo.*`)
#
# The IncludedResourceParams class has three public methods making up its API.
#
# [included_resources]
# returns an array of non-wildcard included elements.
# [has_included_resources?]
# Returns true if our supplied param has included resources, false otherwise.
# [model_includes]
# returns an array suitable to supply to ActiveRecord's `includes` method
# (http://guides.rubyonrails.org/active_record_querying.html#eager-loading-multiple-associations)
# The included_resources should be transformed as specified in the unit tests
# included herein.
#
# All three public methods have unit tests written below that must pass. You are
# free to add additional classes/modules as necessary and/or private methods
# within the IncludedResourceParams class.
#
# Feel free to use the Ruby standard libraries available on codepad in your
# solution.
#
# Create your solution as a private fork, and send us the URL.
#
class IncludedResourceParams
RESOURCES_SEPARATOR_CHAR = ','
RESOURCES_REJECTED_CHARS = %w(\*)
# @!attribute [r] include_param
# @return [String]
attr_reader :include_param
# @param include_param [String]
def initialize(include_param)
@include_param = include_param
end
##
# Does our IncludedResourceParams instance actually have any valid included
# resources after parsing?
#
# @return [Boolean] whether this instance has included resources
def has_included_resources?
@has_included_resources ||= included_resources.any?
end
##
# Fetches the included resourcs as an Array containing only non-wildcard
# resource specifiers.
#
# @example nil
# IncludedResourceParams.new(nil).included_resources => []
#
# @example "foo,foo.bar,baz.*"
# IncludedResourceParams.new("foo,bar,baz.*").included_resources => ["foo", "foo.bar"]
#
# @return [Array<String>] an Array of Strings parsed from the include param with
# wildcard includes removed
def included_resources
@included_resources ||= begin
return [] if include_param.nil?
include_param.split(RESOURCES_SEPARATOR_CHAR).uniq.reject { |resource| invalid_resource?(resource) }
end
end
##
# Converts the resources to be included from their JSONAPI representation to
# a structure compatible with ActiveRecord's `includes` methods. This can/should
# be an Array in all cases. Does not do any verification that the resources
# specified for inclusion are actual ActiveRecord classes.
#
# @example nil
# IncludedResourceParams.new(nil).model_includes => []
#
# @example "foo"
# IncludedResourceParams.new("foo").model_includes => [:foo]
#
# @see Following unit tests
#
# @return [Array] an Array of Symbols and/or Hashes compatible with ActiveRecord
# `includes`
def model_includes
@model_includes ||= parse_for_active_record_includes(resources: included_resources)
end
private
##
# Check if the given resource have any invalid character
#
# @param resource [String]
# @return [Boolean]
def invalid_resource?(resource)
!!resource.match(rejected_characters_regexp)
end
##
# A regexp used to ignore resources with invalid characters in #included_resources
#
# @return [Regexp]
def rejected_characters_regexp
@rejected_characters_regexp ||= %r{(#{RESOURCES_REJECTED_CHARS.join('|')})}
end
##
# Recursively, parses the given resources to ActiveRecord's `includes` method format
#
# @param resources [Array<String>] an Array with resources to be parsed
# @param parent_relationships [Array<Object>] an Array contaning the relationships from the previous recursion iteration
# @return [Array] an Array of Symbols and/or Hashes compatible with ActiveRecord's `includes` method
def parse_for_active_record_includes(resources:, parent_relationships: [])
resources.each do |resource|
if matches = resource.match(/^(?<relationship_name>.+?)\.(?<nested_relationships>.+)$/)
relationship_name = matches[:relationship_name].to_sym
nested_relationships = matches[:nested_relationships]
# To ensure that it won't return an Array with repeated relationships. e.g. [:foo, {foo: [:bar]}]
parent_relationships.delete(relationship_name)
current_relationships = find_hash_with_key(array: parent_relationships, key: relationship_name) if parent_relationships.any?
if !current_relationships
current_relationships = { relationship_name => [] }
parent_relationships << current_relationships
end
parse_for_active_record_includes(resources: [nested_relationships], parent_relationships: current_relationships[relationship_name])
else
relationship_name = resource.to_sym
unless find_hash_with_key(array: parent_relationships, key: relationship_name)
parent_relationships << relationship_name
end
end
end
parent_relationships
end
##
# Finds the Hash in the given `array` with the given `key`
#
# @param array [Array] an Array of Symbols and/or Hashes
# @param key [String] the `key` to be found
# @return [Hash, nil] the Hash which contains the given `key`
def find_hash_with_key(array:, key:)
array.find { |element| element.is_a?(Hash) && element.has_key?(key) }
end
end