globalid 0.3.0 → 0.3.1
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of globalid might be problematic. Click here for more details.
- checksums.yaml +5 -5
- data/lib/global_id.rb +1 -0
- data/lib/global_id/global_id.rb +12 -35
- data/lib/global_id/identification.rb +1 -1
- data/lib/global_id/locator.rb +50 -1
- data/lib/global_id/uri/gid.rb +163 -0
- metadata +10 -9
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: db37405060de11d6af715c038fa47349a94f23d6
|
4
|
+
data.tar.gz: a2fc2891e16182cf893b690731c193659e03b349
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5be8760a5728d9ff3b3c3cd6d8cae89d009fd9f9152449211fe0107fc92d56ef34c8357b9e7d565aed5f1dd0c7e5d3981ed56c056d403076b682ddc7891dbaba
|
7
|
+
data.tar.gz: f6a1fec5024a193076ebe07076163685abccc6e4348b2afa67f90bbd1dfc080fe6858de3391b7693956f29b196cb735d0012168e6722be9c7de2ab176556cddc
|
data/lib/global_id.rb
CHANGED
data/lib/global_id/global_id.rb
CHANGED
@@ -2,16 +2,20 @@ require 'active_support'
|
|
2
2
|
require 'active_support/core_ext/string/inflections' # For #model_class constantize
|
3
3
|
require 'active_support/core_ext/array/access'
|
4
4
|
require 'active_support/core_ext/object/try' # For #find
|
5
|
-
require '
|
5
|
+
require 'active_support/core_ext/module/delegation'
|
6
|
+
require 'global_id/uri/gid'
|
6
7
|
|
7
8
|
class GlobalID
|
8
9
|
class << self
|
9
10
|
attr_reader :app
|
10
11
|
|
11
12
|
def create(model, options = {})
|
12
|
-
app = options.fetch
|
13
|
-
|
14
|
-
|
13
|
+
if app = options.fetch(:app) { GlobalID.app }
|
14
|
+
new URI::GID.create(app, model), options
|
15
|
+
else
|
16
|
+
raise ArgumentError, 'An app is required to create a GlobalID. ' \
|
17
|
+
'Pass the :app option or set the default GlobalID.app.'
|
18
|
+
end
|
15
19
|
end
|
16
20
|
|
17
21
|
def find(gid, options = {})
|
@@ -25,14 +29,7 @@ class GlobalID
|
|
25
29
|
end
|
26
30
|
|
27
31
|
def app=(app)
|
28
|
-
@app = validate_app(app)
|
29
|
-
end
|
30
|
-
|
31
|
-
def validate_app(app)
|
32
|
-
URI.parse('gid:///').hostname = app
|
33
|
-
rescue URI::InvalidComponentError
|
34
|
-
raise ArgumentError, 'Invalid app name. ' \
|
35
|
-
'App names must be valid URI hostnames: alphanumeric and hyphen characters only.'
|
32
|
+
@app = URI::GID.validate_app(app)
|
36
33
|
end
|
37
34
|
|
38
35
|
private
|
@@ -47,10 +44,11 @@ class GlobalID
|
|
47
44
|
end
|
48
45
|
end
|
49
46
|
|
50
|
-
attr_reader :uri
|
47
|
+
attr_reader :uri
|
48
|
+
delegate :app, :model_name, :model_id, :params, :to_s, to: :uri
|
51
49
|
|
52
50
|
def initialize(gid, options = {})
|
53
|
-
|
51
|
+
@uri = gid.is_a?(URI::GID) ? gid : URI::GID.parse(gid)
|
54
52
|
end
|
55
53
|
|
56
54
|
def find(options = {})
|
@@ -65,29 +63,8 @@ class GlobalID
|
|
65
63
|
other.is_a?(GlobalID) && @uri == other.uri
|
66
64
|
end
|
67
65
|
|
68
|
-
def to_s
|
69
|
-
@uri.to_s
|
70
|
-
end
|
71
|
-
|
72
66
|
def to_param
|
73
67
|
# remove the = padding character for a prettier param -- it'll be added back in parse_encoded_gid
|
74
68
|
Base64.urlsafe_encode64(to_s).sub(/=+$/, '')
|
75
69
|
end
|
76
|
-
|
77
|
-
private
|
78
|
-
PATH_REGEXP = %r(\A/([^/]+)/([^/]+)\z)
|
79
|
-
|
80
|
-
# Pending a URI::GID to handle validation
|
81
|
-
def extract_uri_components(gid)
|
82
|
-
@uri = gid.is_a?(URI) ? gid : URI.parse(gid)
|
83
|
-
raise URI::BadURIError, "Not a gid:// URI scheme: #{@uri.inspect}" unless @uri.scheme == 'gid'
|
84
|
-
|
85
|
-
if @uri.path =~ PATH_REGEXP
|
86
|
-
@app = @uri.host
|
87
|
-
@model_name = $1
|
88
|
-
@model_id = $2
|
89
|
-
else
|
90
|
-
raise URI::InvalidURIError, "Expected a URI like gid://app/Person/1234: #{@uri.inspect}"
|
91
|
-
end
|
92
|
-
end
|
93
70
|
end
|
data/lib/global_id/locator.rb
CHANGED
@@ -1,3 +1,6 @@
|
|
1
|
+
require 'active_support'
|
2
|
+
require 'active_support/core_ext/enumerable' # For Enumerable#index_by
|
3
|
+
|
1
4
|
class GlobalID
|
2
5
|
module Locator
|
3
6
|
class << self
|
@@ -15,6 +18,31 @@ class GlobalID
|
|
15
18
|
end
|
16
19
|
end
|
17
20
|
|
21
|
+
# Takes an array of GlobalIDs or strings that can be turned into a GlobalIDs.
|
22
|
+
# The GlobalIDs are located using Model.find(array_of_ids), so the models must respond to
|
23
|
+
# that finder signature.
|
24
|
+
#
|
25
|
+
# This approach will efficiently call only one #find per model class, but still interpolate
|
26
|
+
# the results to match the order in which the gids were passed.
|
27
|
+
#
|
28
|
+
# Options:
|
29
|
+
# * <tt>:only</tt> - A class, module or Array of classes and/or modules that are
|
30
|
+
# allowed to be located. Passing one or more classes limits instances of returned
|
31
|
+
# classes to those classes or their subclasses. Passing one or more modules in limits
|
32
|
+
# instances of returned classes to those including that module. If no classes or
|
33
|
+
# modules match, +nil+ is returned.
|
34
|
+
def locate_many(gids, options = {})
|
35
|
+
if (allowed_gids = parse_allowed(gids, options[:only])).any?
|
36
|
+
models_and_ids = allowed_gids.collect { |gid| [ gid.model_name.constantize, gid.model_id ] }
|
37
|
+
ids_by_model = models_and_ids.group_by(&:first)
|
38
|
+
loaded_by_model = Hash[ids_by_model.map { |model, ids| [ model, model.find(ids.map(&:last)).index_by { |record| record.id.to_s } ] }]
|
39
|
+
|
40
|
+
models_and_ids.collect { |(model, id)| loaded_by_model[model][id] }.compact
|
41
|
+
else
|
42
|
+
[]
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
18
46
|
# Takes either a SignedGlobalID or a string that can be turned into a SignedGlobalID
|
19
47
|
#
|
20
48
|
# Options:
|
@@ -27,6 +55,23 @@ class GlobalID
|
|
27
55
|
SignedGlobalID.find sgid, options
|
28
56
|
end
|
29
57
|
|
58
|
+
# Takes an array of SignedGlobalIDs or strings that can be turned into a SignedGlobalIDs.
|
59
|
+
# The SignedGlobalIDs are located using Model.find(array_of_ids), so the models must respond to
|
60
|
+
# that finder signature.
|
61
|
+
#
|
62
|
+
# This approach will efficiently call only one #find per model class, but still interpolate
|
63
|
+
# the results to match the order in which the gids were passed.
|
64
|
+
#
|
65
|
+
# Options:
|
66
|
+
# * <tt>:only</tt> - A class, module or Array of classes and/or modules that are
|
67
|
+
# allowed to be located. Passing one or more classes limits instances of returned
|
68
|
+
# classes to those classes or their subclasses. Passing one or more modules in limits
|
69
|
+
# instances of returned classes to those including that module. If no classes or
|
70
|
+
# modules match, +nil+ is returned.
|
71
|
+
def locate_many_signed(sgids, options = {})
|
72
|
+
locate_many sgids.collect { |sgid| SignedGlobalID.parse(sgid) }, options
|
73
|
+
end
|
74
|
+
|
30
75
|
# Tie a locator to an app.
|
31
76
|
# Useful when different apps collaborate and reference each others' Global IDs.
|
32
77
|
#
|
@@ -50,7 +95,7 @@ class GlobalID
|
|
50
95
|
def use(app, locator = nil, &locator_block)
|
51
96
|
raise ArgumentError, 'No locator provided. Pass a block or an object that responds to #locate.' unless locator || block_given?
|
52
97
|
|
53
|
-
|
98
|
+
URI::GID.validate_app(app)
|
54
99
|
|
55
100
|
@locators[normalize_app(app)] = locator || BlockLocator.new(locator_block)
|
56
101
|
end
|
@@ -64,6 +109,10 @@ class GlobalID
|
|
64
109
|
only ? Array(only).any? { |c| model_class <= c } : true
|
65
110
|
end
|
66
111
|
|
112
|
+
def parse_allowed(gids, only = nil)
|
113
|
+
gids.collect { |gid| GlobalID.parse(gid) }.compact.select { |gid| find_allowed?(gid.model_class, only) }
|
114
|
+
end
|
115
|
+
|
67
116
|
def normalize_app(app)
|
68
117
|
app.to_s.downcase
|
69
118
|
end
|
@@ -0,0 +1,163 @@
|
|
1
|
+
require 'uri/generic'
|
2
|
+
require 'active_support/core_ext/module/aliasing'
|
3
|
+
require 'active_support/core_ext/object/blank'
|
4
|
+
require 'active_support/core_ext/hash/indifferent_access'
|
5
|
+
|
6
|
+
module URI
|
7
|
+
class GID < Generic
|
8
|
+
# URI::GID encodes an app unique reference to a specific model as an URI.
|
9
|
+
# It has the components: app name, model class name, model id and params.
|
10
|
+
# All components except params are required.
|
11
|
+
#
|
12
|
+
# The URI format looks like "gid://app/model_name/model_id".
|
13
|
+
#
|
14
|
+
# Simple metadata can be stored in params. Useful if your app has multiple databases,
|
15
|
+
# for instance, and you need to find out which one to look up the model in.
|
16
|
+
#
|
17
|
+
# Params will be encoded as query parameters like so
|
18
|
+
# "gid://app/model_name/model_id?key=value&another_key=another_value".
|
19
|
+
#
|
20
|
+
# Params won't be typecast, they're always strings.
|
21
|
+
# For convenience params can be accessed using both strings and symbol keys.
|
22
|
+
#
|
23
|
+
# Multi value params aren't supported. Any params encoding multiple values under
|
24
|
+
# the same key will return only the last value. For example, when decoding
|
25
|
+
# params like "key=first_value&key=last_value" key will only be last_value.
|
26
|
+
#
|
27
|
+
# Read the documentation for +parse+, +create+ and +build+ for more.
|
28
|
+
alias :app :host
|
29
|
+
attr_reader :model_name, :model_id, :params
|
30
|
+
|
31
|
+
class << self
|
32
|
+
# Validates +app+'s as URI hostnames containing only alphanumeric characters
|
33
|
+
# and hyphens. An ArgumentError is raised if +app+ is invalid.
|
34
|
+
#
|
35
|
+
# URI::GID.validate_app('bcx') # => 'bcx'
|
36
|
+
# URI::GID.validate_app('foo-bar') # => 'foo-bar'
|
37
|
+
#
|
38
|
+
# URI::GID.validate_app(nil) # => ArgumentError
|
39
|
+
# URI::GID.validate_app('foo/bar') # => ArgumentError
|
40
|
+
def validate_app(app)
|
41
|
+
parse("gid://#{app}/Model/1").app
|
42
|
+
rescue URI::Error
|
43
|
+
raise ArgumentError, 'Invalid app name. ' \
|
44
|
+
'App names must be valid URI hostnames: alphanumeric and hyphen characters only.'
|
45
|
+
end
|
46
|
+
|
47
|
+
# Create a new URI::GID by parsing a gid string with argument check.
|
48
|
+
#
|
49
|
+
# URI::GID.parse 'gid://bcx/Person/1?key=value'
|
50
|
+
#
|
51
|
+
# This differs from URI() and URI.parse which do not check arguments.
|
52
|
+
#
|
53
|
+
# URI('gid://bcx') # => URI::GID instance
|
54
|
+
# URI.parse('gid://bcx') # => URI::GID instance
|
55
|
+
# URI::GID.parse('gid://bcx/') # => raises URI::InvalidComponentError
|
56
|
+
def parse(uri)
|
57
|
+
generic_components = URI.split(uri) << nil << true # nil parser, true arg_check
|
58
|
+
new(*generic_components)
|
59
|
+
end
|
60
|
+
|
61
|
+
# Shorthand to build a URI::GID from an app, a model and optional params.
|
62
|
+
#
|
63
|
+
# URI::GID.create('bcx', Person.find(5), database: 'superhumans')
|
64
|
+
def create(app, model, params = nil)
|
65
|
+
build app: app, model_name: model.class.name, model_id: model.id, params: params
|
66
|
+
end
|
67
|
+
|
68
|
+
# Create a new URI::GID from components with argument check.
|
69
|
+
#
|
70
|
+
# The allowed components are app, model_name, model_id and params, which can be
|
71
|
+
# either a hash or an array.
|
72
|
+
#
|
73
|
+
# Using a hash:
|
74
|
+
#
|
75
|
+
# URI::GID.build(app: 'bcx', model_name: 'Person', model_id: '1', params: { key: 'value' })
|
76
|
+
#
|
77
|
+
# Using an array, the arguments must be in order [app, model_name, model_id, params]:
|
78
|
+
#
|
79
|
+
# URI::GID.build(['bcx', 'Person', '1', key: 'value'])
|
80
|
+
def build(args)
|
81
|
+
parts = Util.make_components_hash(self, args)
|
82
|
+
parts[:host] = parts[:app]
|
83
|
+
parts[:path] = "/#{parts[:model_name]}/#{parts[:model_id]}"
|
84
|
+
parts[:query] = URI.encode_www_form(parts[:params]) if parts[:params]
|
85
|
+
|
86
|
+
super parts
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def to_s
|
91
|
+
# Implement #to_s to avoid no implicit conversion of nil into string when path is nil
|
92
|
+
"gid://#{app}#{path}#{'?' + query if query}"
|
93
|
+
end
|
94
|
+
|
95
|
+
protected
|
96
|
+
def set_path(path)
|
97
|
+
set_model_components(path) unless defined?(@model_name) && @model_id
|
98
|
+
super
|
99
|
+
end
|
100
|
+
|
101
|
+
# Ruby 2.2 uses #query= instead of #set_query
|
102
|
+
def query=(query)
|
103
|
+
set_params parse_query_params(query)
|
104
|
+
super
|
105
|
+
end
|
106
|
+
|
107
|
+
# Ruby 2.1 or less uses #set_query to assign the query
|
108
|
+
def set_query(query)
|
109
|
+
set_params parse_query_params(query)
|
110
|
+
super
|
111
|
+
end
|
112
|
+
|
113
|
+
def set_params(params)
|
114
|
+
@params = params
|
115
|
+
end
|
116
|
+
|
117
|
+
private
|
118
|
+
COMPONENT = [ :scheme, :app, :model_name, :model_id, :params ].freeze
|
119
|
+
|
120
|
+
# Extracts model_name and model_id from the URI path.
|
121
|
+
PATH_REGEXP = %r(\A/([^/]+)/?([^/]+)?\z)
|
122
|
+
|
123
|
+
def check_host(host)
|
124
|
+
validate_component(host)
|
125
|
+
super
|
126
|
+
end
|
127
|
+
|
128
|
+
def check_path(path)
|
129
|
+
validate_component(path)
|
130
|
+
set_model_components(path, true)
|
131
|
+
end
|
132
|
+
|
133
|
+
def check_scheme(scheme)
|
134
|
+
if scheme == 'gid'
|
135
|
+
super
|
136
|
+
else
|
137
|
+
raise URI::BadURIError, "Not a gid:// URI scheme: #{inspect}"
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def set_model_components(path, validate = false)
|
142
|
+
_, model_name, model_id = path.match(PATH_REGEXP).to_a
|
143
|
+
|
144
|
+
validate_component(model_name) && validate_component(model_id) if validate
|
145
|
+
|
146
|
+
@model_name = model_name
|
147
|
+
@model_id = model_id
|
148
|
+
end
|
149
|
+
|
150
|
+
def validate_component(component)
|
151
|
+
return component unless component.blank?
|
152
|
+
|
153
|
+
raise URI::InvalidComponentError,
|
154
|
+
"Expected a URI like gid://app/Person/1234: #{inspect}"
|
155
|
+
end
|
156
|
+
|
157
|
+
def parse_query_params(query)
|
158
|
+
Hash[URI.decode_www_form(query)].with_indifferent_access if query
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
@@schemes['GID'] = GID
|
163
|
+
end
|
metadata
CHANGED
@@ -1,41 +1,41 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: globalid
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.3.
|
4
|
+
version: 0.3.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- David Heinemeier Hansson
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2015-02-05 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
|
-
- -
|
17
|
+
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
19
|
version: 4.1.0
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
|
-
- -
|
24
|
+
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: 4.1.0
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: rake
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
|
-
- -
|
31
|
+
- - ">="
|
32
32
|
- !ruby/object:Gem::Version
|
33
33
|
version: '0'
|
34
34
|
type: :development
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
|
-
- -
|
38
|
+
- - ">="
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '0'
|
41
41
|
description: URIs for your models makes it easy to pass references around.
|
@@ -51,6 +51,7 @@ files:
|
|
51
51
|
- lib/global_id/locator.rb
|
52
52
|
- lib/global_id/railtie.rb
|
53
53
|
- lib/global_id/signed_global_id.rb
|
54
|
+
- lib/global_id/uri/gid.rb
|
54
55
|
- lib/globalid.rb
|
55
56
|
homepage: http://www.rubyonrails.org
|
56
57
|
licenses:
|
@@ -62,12 +63,12 @@ require_paths:
|
|
62
63
|
- lib
|
63
64
|
required_ruby_version: !ruby/object:Gem::Requirement
|
64
65
|
requirements:
|
65
|
-
- -
|
66
|
+
- - ">="
|
66
67
|
- !ruby/object:Gem::Version
|
67
68
|
version: 1.9.3
|
68
69
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
69
70
|
requirements:
|
70
|
-
- -
|
71
|
+
- - ">="
|
71
72
|
- !ruby/object:Gem::Version
|
72
73
|
version: '0'
|
73
74
|
requirements: []
|
@@ -75,5 +76,5 @@ rubyforge_project:
|
|
75
76
|
rubygems_version: 2.2.2
|
76
77
|
signing_key:
|
77
78
|
specification_version: 4
|
78
|
-
summary:
|
79
|
+
summary: 'Refer to any model with a URI: gid://app/class/id'
|
79
80
|
test_files: []
|