-
-
Notifications
You must be signed in to change notification settings - Fork 9
/
uid.rb
172 lines (140 loc) · 5.5 KB
/
uid.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
# frozen_string_literal: true
require "uri"
unless defined?(::URI::UID) || ::URI.scheme_list.include?("UID")
module URI
class UID < ::URI::Generic
VERSION = UniversalID::VERSION
SCHEME = "uid"
HOST = "universalid"
PATTERN = /\A#{SCHEME}:\/\/#{HOST}\/[-_0-9A-Z]+#[-_0-9A-Z]+\z/io
class << self
def encoder
UniversalID::Encoder
end
def fingerprint(object)
encode fingerprint_components(object)
end
def parse(value)
return nil if value.nil?
return value if value.is_a?(self)
value = value.to_s
return nil if value.strip.empty?
new(*::URI.split(value))
end
def match?(uri)
return true if uri.is_a?(self)
uri.to_s.match? PATTERN
end
def build_string(payload, object = nil)
"#{SCHEME}:https://#{HOST}/#{payload}##{fingerprint(object)}"
end
def build(object, options = {}, &block)
path = "/#{encode(object, options, &block)}"
parse "#{SCHEME}:https://#{HOST}#{path}##{fingerprint(object)}"
end
def from_payload(payload, object = nil)
parse(build_string(payload, object)).tap do |uid|
# NOTE: fingerprint mismatch can happen when building from a UID payload
# ensure the fingerprint is correct
if uid&.valid? && URI::UID.fingerprint(uid.decode) != uid.fingerprint
remove_instance_variable :@decoded_fingerprint if instance_variable_defined?(:@decoded_fingerprint)
uid.instance_variable_set :@fragment, URI::UID.build(uid.decode).fingerprint
end
end
end
def encode(object, options = {})
return yield(object, options) if block_given?
encoder.encode object, options
end
def decode(...)
encoder.decode(...)
end
# Creates a new URI::UID with the given URI components.
# SEE: https://ruby-doc.org/3.2.2/stdlibs/uri/URI/Generic.html#method-c-new
#
# @param scheme [String] the scheme component.
# @param userinfo [String] the userinfo component.
# @param host [String] the host component.
# @param port [Integer] the port component.
# @param registry [String] the registry component.
# @param path [String] the path component.
# @param opaque [String] the opaque component.
# @param query [String] the query component.
# @param fragment [String] the fragment component.
# @param parser [URI::Parser] the parser to use for the URI, defaults to DEFAULT_PARSER.
# @param arg_check [Boolean] whether to check arguments, defaults to false.
# @return [URI::UID] the new URI::UID instance.
# # @raise [URI::InvalidURIError] if the URI is malformed.
# @raise [ArgumentError] if the number of arguments is incorrect or an argument is of the wrong type.
# @raise [TypeError] if an argument is not of the expected type.
# @raise [URI::InvalidComponentError] if a component of the URI is not valid.
# @raise [URI::BadURIError] if the URI is in a bad or unexpected state.
def new(...)
super.tap do |uri|
if uri.invalid?
raise ::URI::InvalidComponentError, "Scheme must be `#{SCHEME}`" if uri.scheme != SCHEME
raise ::URI::InvalidComponentError, "Host must be `#{HOST}`" if uri.host != HOST
raise ::URI::InvalidComponentError, "Unable to parse `payload` from the path component!" if uri.payload.strip.empty?
end
end
end
private
def fingerprint_components(object)
klass = object.is_a?(Class) ? object : object.class
tokens = [klass]
begin
path = const_source_location(klass.name).first.to_s
tokens << ::File.mtime(path).utc if ::File.exist?(path)
rescue => e
UniversalID.logger&.warn "URI::UID#fingerprint: Unable to determine the source location for #{klass.name}!\n#{e.message}}"
end
tokens
end
end
def payload
path[1..]
end
def fingerprint(decode: false)
return @decoded_fingerprint ||= decode_fingerprint if decode
fragment
end
def valid?
case self
in scheme: SCHEME, host: HOST, path: p, fragment: _ if p.size >= 8 then true
else false
end
end
def invalid?
!valid?
end
def decode(force: false)
return nil unless valid?
remove_instance_variable :@decoded if force && instance_variable_defined?(:@decoded)
return @decoded if defined?(@decoded)
@decoded ||= yield(decode_payload, *decode_fingerprint) if block_given?
@decoded ||= decode_payload
end
def deconstruct_keys(_keys)
{scheme: scheme, host: host, path: path, fragment: fragment}
end
def inspect
"#<URI::UID payload=#{payload.truncate 40}, fingerprint=#{fingerprint.truncate 40}>"
end
private
def decode_payload
self.class.decode payload
end
def decode_fingerprint
self.class.decode fingerprint
end
end
# Register the URI scheme
if ::URI.respond_to? :register_scheme
::URI.register_scheme "UID", UID unless ::URI.scheme_list.include?("UID")
else
# shenanigans to support Ruby 3.0.X
::URI::UID = UID unless defined?(::URI::UID)
::URI.scheme_list["UID"] = UID
end
end
end