Skip to content

Instantly share code, notes, and snippets.

@a-ayyash
Last active November 3, 2016 16:17
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save a-ayyash/8dcff722dde6fa258ca8d51169aca1fb to your computer and use it in GitHub Desktop.
Save a-ayyash/8dcff722dde6fa258ca8d51169aca1fb to your computer and use it in GitHub Desktop.
class JSONTrie
def initialize()
@root = JSONTrieNode.new("", nil)
end
def add(entities)
list_of_entities = entities.split('.')
@root.addEntities(list_of_entities)
end
def read()
@root.traverseAndParse
end
private
class JSONTrieNode
attr_reader :children
def initialize(word, parent)
@word = word
@children = {}
@parent = parent
end
def traverseAndParse
result = []
@children.each_pair do |k, v|
if v.children.count == 0
result << v.to_sym_array
else
result << Hash[k.to_sym, v.readTillEnd]
end
end
result.flatten
end
def readTillEnd
if @children.keys.count > 1
symbolize_items(@children.keys)
else
node = @children[@word]
if node
result = @children[@word].readTillEnd
if result.empty?
return to_sym_array
end
construct_response_with_result(result)
else
""
end
end
end
def symbolize_items(list)
list.map { |e| e.to_sym }
end
def to_sym_array
[@word.to_sym]
end
def construct_response_with_result(result)
[Hash[@word.to_sym, result]]
end
def addEntities(entities)
if entities.empty?
return
end
@word = entities[0]
shorter_list = entities[1..-1]
if @children[@word].nil?
@children[@word] = JSONTrieNode.new(@word, self)
end
@children[@word].addEntities(shorter_list)
end
end
end
##
# 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
def initialize(include_param)
@include_param = sanitize_input(include_param)
@jsonTrie = JSONTrie.new
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?
!@include_param.empty?
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] an Array of Strings parsed from the include param with
# wildcard includes removed
def included_resources
@include_param
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
@include_param.each {|e| @jsonTrie.add(e)}
@jsonTrie.read
end
private
def sanitize_input(input_str)
unless input_str.nil?
splitted_arr = input_str.split(",")
splitted_arr = splitted_arr.map { |str| str unless str.index("*") != nil }
splitted_arr.compact
else
[]
end
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
class TestJSONTrie < Test::Unit::TestCase
attr_reader :trie
def setup
@trie = JSONTrie.new
end
def test_only_one_element
@trie.add("onlyOneElement")
assert @trie.read ==[:onlyOneElement]
end
def test_shorter_adds_dont_change
@trie.add("first.second.third.forth")
@trie.add("first")
@trie.add("first.second")
@trie.add("first.second.third")
assert @trie.read ==[{:first=>[{:second=>[{:third=>[:forth]}]}]}]
end
def test_changing_the_last_aggregates
@trie.add("first.second.third.forth")
@trie.add("first.second.third.fifth")
assert @trie.read == [{:first=>[{:second=>[{:third=>[:forth, :fifth]}]}]}]
end
def test_two_different_roots
@trie.add("first.second.third.forth")
@trie.add("A.B.C")
assert @trie.read == [{:first=>[{:second=>[{:third=>[:forth]}]}]}, {:A=>[{:B=>[:C]}]}]
end
def test_two_elements
@trie.add("A.B")
assert @trie.read == [{:A=>[:B]}]
end
def test_mix
@trie.add("foo.bar.baz")
@trie.add("foo")
@trie.add("foo.bar.bat")
@trie.add("bar")
assert @trie.read == [{: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