/
gid.rb
181 lines (154 loc) · 6.03 KB
/
gid.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
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
require 'uri/generic'
require 'active_support/core_ext/module/aliasing'
require 'active_support/core_ext/object/blank'
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 the components: app name, model class name, model id and params.
# All components except params are required.
#
# The URI format looks like "gid://app/model_name/model_id".
#
# Simple metadata can be stored in params. Useful if your app has multiple databases,
# for instance, and you need to find out which one to look up the model in.
#
# Params will be encoded as query parameters like so
# "gid://app/model_name/model_id?key=value&another_key=another_value".
#
# Params won't be typecast, they're always strings.
# For convenience params can be accessed using both strings and symbol keys.
#
# Multi value params aren't supported. Any params encoding multiple values under
# the same key will return only the last value. For example, when decoding
# params like "key=first_value&key=last_value" key will only be last_value.
#
# Read the documentation for +parse+, +create+ and +build+ for more.
alias :app :host
attr_reader :model_name, :model_id, :params
# Raised when creating a Global ID for a model without an id
class MissingModelIdError < URI::InvalidComponentError; end
class << self
# Validates +app+'s as URI hostnames containing only alphanumeric characters
# and hyphens. An ArgumentError is raised if +app+ is invalid.
#
# URI::GID.validate_app('bcx') # => 'bcx'
# URI::GID.validate_app('foo-bar') # => 'foo-bar'
#
# URI::GID.validate_app(nil) # => ArgumentError
# URI::GID.validate_app('foo/bar') # => ArgumentError
def validate_app(app)
parse("gid://#{app}/Model/1").app
rescue URI::Error
raise ArgumentError, 'Invalid app name. ' \
'App names must be valid URI hostnames: alphanumeric and hyphen characters only.'
end
# Create a new URI::GID by parsing a gid string with argument check.
#
# URI::GID.parse 'gid://bcx/Person/1?key=value'
#
# This differs from URI() and URI.parse which do not check arguments.
#
# URI('gid://bcx') # => URI::GID instance
# URI.parse('gid://bcx') # => URI::GID instance
# URI::GID.parse('gid://bcx/') # => raises URI::InvalidComponentError
def parse(uri)
generic_components = URI.split(uri) << nil << true # nil parser, true arg_check
new(*generic_components)
end
# Shorthand to build a URI::GID from an app, a model and optional params.
#
# URI::GID.create('bcx', Person.find(5), database: 'superhumans')
def create(app, model, params = nil)
build app: app, model_name: model.class.name, model_id: model.id, params: params
end
# Create a new URI::GID from components with argument check.
#
# The allowed components are app, model_name, model_id and params, which can be
# either a hash or an array.
#
# Using a hash:
#
# URI::GID.build(app: 'bcx', model_name: 'Person', model_id: '1', params: { key: 'value' })
#
# Using an array, the arguments must be in order [app, model_name, model_id, params]:
#
# 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]}/#{CGI.escape(parts[:model_id].to_s)}"
if parts[:params] && !parts[:params].empty?
parts[:query] = URI.encode_www_form(parts[:params])
end
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}#{path}#{'?' + query if query}"
end
protected
def set_path(path)
set_model_components(path) unless defined?(@model_name) && @model_id
super
end
# Ruby 2.2 uses #query= instead of #set_query
def query=(query)
set_params parse_query_params(query)
super
end
# Ruby 2.1 or less uses #set_query to assign the query
def set_query(query)
set_params parse_query_params(query)
super
end
def set_params(params)
@params = params
end
private
COMPONENT = [ :scheme, :app, :model_name, :model_id, :params ].freeze
# Extracts model_name and model_id from the URI path.
PATH_REGEXP = %r(\A/([^/]+)/?([^/]+)?\z)
def check_host(host)
validate_component(host)
super
end
def check_path(path)
validate_component(path)
set_model_components(path, true)
end
def check_scheme(scheme)
if scheme == 'gid'
super
else
raise URI::BadURIError, "Not a gid:// URI scheme: #{inspect}"
end
end
def set_model_components(path, validate = false)
_, model_name, model_id = path.match(PATH_REGEXP).to_a
model_id = CGI.unescape(model_id) if model_id
validate_component(model_name) && validate_model_id(model_id, model_name) if validate
@model_name = model_name
@model_id = model_id
end
def validate_component(component)
return component unless component.blank?
raise URI::InvalidComponentError,
"Expected a URI like gid://app/Person/1234: #{inspect}"
end
def validate_model_id(model_id, model_name)
return model_id unless model_id.blank?
raise MissingModelIdError, "Unable to create a Global ID for " \
"#{model_name} without a model id."
end
def parse_query_params(query)
Hash[URI.decode_www_form(query)].with_indifferent_access if query
end
end
if respond_to?(:register_scheme)
register_scheme('GID', GID)
else
@@schemes['GID'] = GID
end
end