Skip to content

Commit

Permalink
Add options to URI::GID to store simple metadata as query parameters.
Browse files Browse the repository at this point in the history
  • Loading branch information
kaspth committed Sep 26, 2014
1 parent 608c2b3 commit a3f3c0c
Show file tree
Hide file tree
Showing 4 changed files with 73 additions and 18 deletions.
2 changes: 1 addition & 1 deletion lib/global_id/global_id.rb
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ def repad_gid(gid)
end

attr_reader :uri
delegate :app, :model_name, :model_id, :to_s, to: :uri
delegate :app, :model_name, :model_id, :options, :to_s, to: :uri

def initialize(gid, options = {})
@uri = gid.is_a?(URI::GID) ? gid : URI::GID.parse(gid)
Expand Down
51 changes: 37 additions & 14 deletions lib/global_id/uri/gid.rb
Original file line number Diff line number Diff line change
@@ -1,17 +1,29 @@
require 'uri/generic'
require 'active_support/core_ext/module/aliasing'
require 'active_support/core_ext/object/blank'
require 'active_support/core_ext/object/to_query'
require 'active_support/core_ext/hash/indifferent_access'

module URI
class GID < Generic
# URI::GID encodes an app unique reference to a specific model as an URI.
# It has three components: the app name, the model's class name and the
# model's id.
# It has the components: app name, model class name, model id and options.
# All components except options are required.
#
# The URI format looks like "gid://app/model_name/model_id".
#
# Simple metadata can be stored in options. Useful if your app has multiple databases,
# for instance, and you need to find out which one to look up the model in.
#
# Options will be encoded as query parameters like so
# "gid://app/model_name/model_id?key=value&another_key=another_value".
#
# Options won't be typecast, they're always strings.
# For convenience options can be accessed using both strings and symbol keys.
#
# Read the documentation for +parse+, +create+ and +build+ for more.
alias :app :host
attr_reader :model_name, :model_id
attr_reader :model_name, :model_id, :options

class << self
# Validates +app+'s as URI hostnames containing only alphanumeric characters
Expand All @@ -31,7 +43,7 @@ def validate_app(app)

# Create a new URI::GID by parsing a gid string with argument check.
#
# URI::GID.parse 'gid://bcx/Person/1'
# URI::GID.parse 'gid://bcx/Person/1?key=value'
#
# This differs from URI() and URI.parse which do not check arguments.
#
Expand All @@ -43,37 +55,38 @@ def parse(uri)
new *generic_components
end

# Shorthand to build a URI::GID from and app and a model.
# Shorthand to build a URI::GID from an app, a model and optionally options.
#
# URI::GID.create('bcx', Person.find(5))
def create(app, model)
build app: app, model_name: model.class.name, model_id: model.id
# URI::GID.create('bcx', Person.find(5), database: 'superhumans')
def create(app, model, options = nil)
build app: app, model_name: model.class.name, model_id: model.id, options: options
end

# Create a new URI::GID from components with argument check.
#
# The allowed components are app, model_name and model_id, which can be
# The allowed components are app, model_name, model_id and options, which can be
# either a hash or an array.
#
# Using a hash:
#
# URI::GID.build(app: 'bcx', model_name: 'Person', model_id: '1')
# URI::GID.build(app: 'bcx', model_name: 'Person', model_id: '1', options: { key: 'value' })
#
# Using an array, the arguments must be in order [app, model_name, model_id]:
# Using an array, the arguments must be in order [app, model_name, model_id, options]:
#
# URI::GID.build(['bcx', 'Person', '1'])
# URI::GID.build(['bcx', 'Person', '1', key: 'value'])
def build(args)
parts = Util.make_components_hash(self, args)
parts[:host] = parts[:app]
parts[:path] = "/#{parts[:model_name]}/#{parts[:model_id]}"
parts[:query] = parts[:options].to_query if parts[:options]

super parts
end
end

def to_s
# Implement #to_s to avoid no implicit conversion of nil into string when path is nil
"gid://#{app}/#{model_name}/#{model_id}"
"gid://#{app}#{path_query}"
end

protected
Expand All @@ -82,8 +95,18 @@ def set_path(path)
super
end

def set_query(query)
super

return unless @query
@options = @query.split('&').each_with_object({}) do |param, h|
k, v = param.split('=')
h[URI.unescape(k)] = URI.unescape(v)
end.with_indifferent_access
end

private
COMPONENT = [ :scheme, :app, :model_name, :model_id ].freeze
COMPONENT = [ :scheme, :app, :model_name, :model_id, :options ].freeze

# Extracts model_name and model_id from the URI path.
PATH_REGEXP = %r(\A/([^/]+)/?([^/]+)?\z)
Expand Down
7 changes: 7 additions & 0 deletions test/cases/global_id_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -187,3 +187,10 @@ class GlobalIDCreationTest < ActiveSupport::TestCase
end
end
end

class GlobalIDCustomOptionsTest < ActiveSupport::TestCase
test 'custom params' do
gid = GlobalID.parse 'gid://bcx/Person/5?hello=world'
assert_equal 'world', gid.options[:hello]
end
end
31 changes: 28 additions & 3 deletions test/cases/uri_gid_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,17 @@ class URI::GIDTest < ActiveSupport::TestCase
end

test 'build' do
array = URI::GID.build(['bcx', 'Person', '5'])
array = URI::GID.build(['bcx', 'Person', '5', nil])
assert array

hash = URI::GID.build(app: 'bcx', model_name: 'Person', model_id: '5')
hash = URI::GID.build(app: 'bcx', model_name: 'Person', model_id: '5', options: nil)
assert hash

assert_equal array, hash
end

test 'build with wrong ordered array creates a wrong ordered gid' do
assert_not_equal @gid_string, URI::GID.build(['Person', '5', 'bcx']).to_s
assert_not_equal @gid_string, URI::GID.build(['Person', '5', 'bcx', nil]).to_s
end

test 'as String' do
Expand Down Expand Up @@ -103,3 +103,28 @@ def assert_invalid_app(value)
assert_raise(ArgumentError) { URI::GID.validate_app(value) }
end
end

class URI::GIDOptionsTest < ActiveSupport::TestCase
setup do
@gid = URI::GID.create('bcx', Person.find(5), hello: 'world')
end

test 'indifferent key access' do
assert_equal 'world', @gid.options[:hello]
assert_equal 'world', @gid.options['hello']
end

test 'integer option' do
gid = URI::GID.build(['bcx', 'Person', '5', integer: 20])
assert_equal '20', gid.options[:integer]
end

test 'as String' do
assert_equal 'gid://bcx/Person/5?hello=world', @gid.to_s
end

test 'immutable options' do
@gid.options[:param] = 'value'
assert_not_equal 'gid://bcx/Person/5?hello=world&param=value', @gid.to_s
end
end

0 comments on commit a3f3c0c

Please sign in to comment.