Skip to content

Instantly share code, notes, and snippets.

@M-Younes
Created June 23, 2017 08:03
Show Gist options
  • Save M-Younes/37b40a23b1c17f9e3cea6ded0a668526 to your computer and use it in GitHub Desktop.
Save M-Younes/37b40a23b1c17f9e3cea6ded0a668526 to your computer and use it in GitHub Desktop.
##
# 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
COMMA_ENDED = /((\w{1,}\.){1,}\*{1,},)/.freeze
COMMA_STARTED = /(,(\w{1,}\.){1,}\*{1,})/.freeze
NO_COMMA = /((\w{1,}\.){1,}\*{1,})/.freeze
WILDCARD_REGEX = [COMMA_ENDED, COMMA_STARTED, NO_COMMA].join("|").freeze
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?
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"] NOTE: I believe there's a typo here the example is "foo,foo.bar,baz.*" NOT "foo,bar,baz.*"
#
# @return [Array] an Array of Strings parsed from the include param with
# wildcard includes removed
def included_resources
# only not empty string passes
return Array.new if @include_param.nil?
return Array.new unless @include_param.is_a?(String) && !@include_param.to_s.strip.empty?
@_included_resources ||= @include_param.gsub(Regexp.new(WILDCARD_REGEX), "").split(",")
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
return Array.new unless included_resources.any?
@_model_includes ||= generate_model_resources
end
private
def generate_model_resources
resources = Array.new
included_resources.each do |element|
element = element.split(".").map(&:to_sym)
merge_elements!(resources, element)
end
resources
end
def merge_elements!(resources, element)
matched = false
resources.map.with_index do |resource, index|
if element.empty?
matched = true
break
end
if resource.is_a? Symbol
if resource == element.first
matched = true
element.size == 1 ? resources[index] = element.first : resources[index] = element.reverse.reduce { |x, y| { y => [x] } }
end
else
if resource.keys.include? element.first
matched = true
resources[index][element.first] = merge_elements!(resource.values.flatten, element.drop(1))
else
next
end
end
end
if matched == false
resources << element.reverse.reduce { |x, y| { y => [x] } }
end
resources
end
end
require 'test/unit'
class TestIncludedResourceParams < Test::Unit::TestCase
# Tests for #has_included_resources?
def test_has_included_resources_is_false_when_nil
r = IncludedResourceParams.new(nil)
assert r.has_included_resources? == false
end
def test_has_included_resources_is_false_when_only_wildcards
include_string = 'foo.**'
r = IncludedResourceParams.new(include_string)
assert r.has_included_resources? == false
end
def test_has_included_resources_is_true_with_non_wildcard_params
include_string = 'foo'
r = IncludedResourceParams.new(include_string)
assert r.has_included_resources?
end
def test_has_included_resources_is_true_with_both_wildcard_and_non_params
include_string = 'foo,bar.**'
r = IncludedResourceParams.new(include_string)
assert r.has_included_resources?
end
# Tests for #included_resources
def test_included_resources_always_returns_array
r = IncludedResourceParams.new(nil)
assert r.included_resources == []
end
def test_included_resources_returns_only_non_wildcards
r = IncludedResourceParams.new('foo,foo.bar,baz.*,bat.**')
assert r.included_resources == ['foo', 'foo.bar']
end
# Tests for #model_includes
def test_model_includes_when_params_nil
assert IncludedResourceParams.new(nil).model_includes == []
end
def test_model_includes_one_single_level_resource
assert IncludedResourceParams.new('foo').model_includes == [:foo]
end
def test_model_includes_multiple_single_level_resources
assert IncludedResourceParams.new('foo,bar').model_includes == [:foo, :bar]
end
def test_model_includes_single_two_level_resource
assert IncludedResourceParams.new('foo.bar').model_includes == [{:foo => [:bar]}]
end
def test_model_includes_multiple_two_level_resources
assert IncludedResourceParams.new('foo.bar,foo.bat').model_includes == [{:foo => [:bar, :bat]}]
assert IncludedResourceParams.new('foo.bar,baz.bat').model_includes == [{:foo => [:bar]}, {:baz => [:bat]}]
end
def test_model_includes_three_level_resources
assert IncludedResourceParams.new('foo.bar.baz').model_includes == [{:foo => [{:bar => [:baz]}]}]
end
def test_model_includes_multiple_three_level_resources
assert IncludedResourceParams.new('foo.bar.baz,foo,foo.bar.bat,bar').model_includes == [{:foo => [{:bar => [:baz, :bat]}]}, :bar]
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment