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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- !binary "U0hBMQ==":
3
- metadata.gz: d325de1fd4887ed3e91ebb2ec6d58d4ed0d552d0
4
- data.tar.gz: e7d0091c1cc92b20c682093faa424b528c32940f
2
+ SHA1:
3
+ metadata.gz: db37405060de11d6af715c038fa47349a94f23d6
4
+ data.tar.gz: a2fc2891e16182cf893b690731c193659e03b349
5
5
  SHA512:
6
- metadata.gz: 98f92c4c3fa1e9a127abcc44f1eb51ca529f5388dec8529aacce43c18a9fc9cc9aa947854620abe0bd609ebb46dfcee22a1d81089bffb83d106d29967c457147
7
- data.tar.gz: 989269225c2d49d7a25cb6bb5e7f9bca1087268d5688d27558bb8d2fa1d1062360bbf72f2d8004cb764c886217f4d6a7eed73da1e0a1fe190b1ba250f1026bcb
6
+ metadata.gz: 5be8760a5728d9ff3b3c3cd6d8cae89d009fd9f9152449211fe0107fc92d56ef34c8357b9e7d565aed5f1dd0c7e5d3981ed56c056d403076b682ddc7891dbaba
7
+ data.tar.gz: f6a1fec5024a193076ebe07076163685abccc6e4348b2afa67f90bbd1dfc080fe6858de3391b7693956f29b196cb735d0012168e6722be9c7de2ab176556cddc
@@ -1,4 +1,5 @@
1
1
  require 'global_id/global_id'
2
+
2
3
  autoload :SignedGlobalID, 'global_id/signed_global_id'
3
4
 
4
5
  class GlobalID
@@ -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 'uri'
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 :app, GlobalID.app
13
- raise ArgumentError, "An app is required to create a GlobalID. Pass the :app option or set the default GlobalID.app." unless app
14
- new URI("gid://#{app}/#{model.class.name}/#{model.id}"), options
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, :app, :model_name, :model_id
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
- extract_uri_components gid
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
@@ -10,7 +10,7 @@ class GlobalID
10
10
  alias to_gid to_global_id
11
11
 
12
12
  def to_signed_global_id(options = {})
13
- @signed_global_id ||= SignedGlobalID.create(self, options)
13
+ SignedGlobalID.create(self, options)
14
14
  end
15
15
  alias to_sgid to_signed_global_id
16
16
  end
@@ -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
- GlobalID.validate_app(app)
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.0
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: 2014-09-12 00:00:00.000000000 Z
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: ! 'Refer to any model with a URI: gid://app/class/id'
79
+ summary: 'Refer to any model with a URI: gid://app/class/id'
79
80
  test_files: []