wayfinding 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: ad2f192ae238999788d4d63f9ae53e60f5897f76ea841c4d6a44292b181a7f5c
4
+ data.tar.gz: acd21e66e0788c34ed59b4e4f83239fa9d4f8a3004756b0d5bb7d2c0584c9ee0
5
+ SHA512:
6
+ metadata.gz: cc7e58f174db34dd517d2e7aa7f1f90797f6de157ac0a2b8bce3a26d000ff9abf52358b0c27acb7fe1c6eccc55dd93d22515133d0cfc12fb50d20c18e85fbe02
7
+ data.tar.gz: 49161506abc7526fe2276ad34b329e0828a8c5f873b042590c1c2ddd3b91833138e61d8717814a6a012ab73f35f02340a74aa42f89d379182af64822336dcddb
data/Rakefile ADDED
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+
5
+ require "rspec/core/rake_task"
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require "rubocop/rake_task"
9
+ RuboCop::RakeTask.new(:rubocop)
10
+
11
+ task default: %i[rubocop spec]
data/docs/README.md ADDED
@@ -0,0 +1,137 @@
1
+ # Wayfinding
2
+
3
+ Cross-application URL resolution.
4
+
5
+ Wayfinding is a global registry of named destinations. It lets one component link to a route owned by
6
+ another without depending on that component.
7
+
8
+ Links are not functionality. No component should acquire a dependency because it renders an anchor tag.
9
+
10
+ ## Why
11
+
12
+ Without a registry, a component that links to another component's page must reach into that engine's
13
+ `url_helpers`, depend on the owner just for the link, or hardcode the path. Each option couples the caller
14
+ to the route's current owner or location; a hardcoded path can also break silently.
15
+
16
+ Wayfinding gives one accessor and one registration. Moving a route becomes a one-line change to the
17
+ registration; every call site is untouched.
18
+
19
+ ## Registering
20
+
21
+ Registration is configuration: every argument is a keyword, so order does not matter. Register from an
22
+ initializer.
23
+
24
+ ```ruby
25
+ Wayfinding.register(
26
+ name: :home,
27
+ engine: -> { Homes::Engine },
28
+ helper: :home_path
29
+ )
30
+ ```
31
+
32
+ `engine:` accepts a lambda or the constant. Prefer the lambda so the engine is resolved lazily.
33
+
34
+ `helper:` is the common form. Wayfinding derives the corresponding `_url` helper for `url_for`.
35
+
36
+ For anything that is not a bare helper call, pass a block. Within the block, `self` is the engine's
37
+ `url_helpers` object. That lets the resolver call `results_path(...)` directly instead of repeating
38
+ `Search::Engine.routes.url_helpers.results_path(...)`.
39
+
40
+ ```ruby
41
+ Wayfinding.register(name: :search_results, engine: -> { Search::Engine }) do |query|
42
+ results_path(q: query)
43
+ end
44
+ ```
45
+
46
+ For `url_for`, calls ending in `_path` inside an engine-backed block are mapped to the corresponding
47
+ `_url` helper. Wayfinding cannot convert an arbitrary path string after the block returns, so a block
48
+ that supports both lookup forms must build its result from route helpers rather than hardcoding a path.
49
+
50
+ Omit `engine:` for a destination that is not a Rails route. Wayfinding calls the block directly, and
51
+ `path_for` and `url_for` both return its result.
52
+
53
+ ## Looking up
54
+
55
+ ```ruby
56
+ Wayfinding.path_for(:home, home, tab: "activity")
57
+ # => "/homes/17?tab=activity"
58
+
59
+ Wayfinding.url_for(:home, home)
60
+ # => "https://example.com/homes/17"
61
+ ```
62
+
63
+ Looking up an unregistered destination raises `Wayfinding::UnregisteredDestination`, listing what is
64
+ registered. There is no null object and no production fallback: a missing registration is a boot-state
65
+ bug, not a runtime condition.
66
+
67
+ ## Kinds
68
+
69
+ A kind is a named field contract. Applications define kinds; Wayfinding only enforces them.
70
+
71
+ ```ruby
72
+ Wayfinding.define_kind(:report, requires: %i[label description action subject])
73
+
74
+ Wayfinding.register(
75
+ name: :activity_report,
76
+ kind: :report,
77
+ engine: -> { Reports::Engine },
78
+ helper: :activity_reports_path,
79
+ action: :view_activity_report,
80
+ subject: -> { Account },
81
+ label: "Activity Report",
82
+ description: "Summarizes recent account activity"
83
+ )
84
+
85
+ Wayfinding.of_kind(:report).accessible_by(current_ability)
86
+ ```
87
+
88
+ A destination without a kind is still resolvable by `path_for`, but is invisible to `of_kind`. Declaring
89
+ a kind does not remove point lookup, so `path_for(:activity_report)` still works.
90
+
91
+ `requires:` may name any field. Validation asserts presence only and never resolves callables, so a
92
+ lazily registered `subject: -> { Account }` passes without autoloading the model.
93
+
94
+ ### Destination metadata
95
+
96
+ Every keyword other than the routing structure is application metadata. Wayfinding does not give fields
97
+ such as `label` or `description` special behavior. Metadata is stored without interpretation and read
98
+ with `#[]`:
99
+
100
+ ```ruby
101
+ destination = Wayfinding.fetch(:activity_report)
102
+ destination[:label]
103
+ destination[:data]
104
+ ```
105
+
106
+ Resolve any callable field lazily with `#value_for`:
107
+
108
+ ```ruby
109
+ destination.value_for(:subject)
110
+ ```
111
+
112
+ `action` and `subject` are metadata with one additional invariant: if either is supplied, both must be.
113
+ `accessible_by(ability)` reads that pair and calls `ability.can?(action, subject)`. Display policy, such as
114
+ falling back from a missing description to a label, belongs to the consuming application.
115
+
116
+ ## Verifying
117
+
118
+ Assert at boot that every expected destination is registered, so a dropped registration fails the boot
119
+ rather than rendering a broken link.
120
+
121
+ ```ruby
122
+ Wayfinding.verify!(%i[home activity_report])
123
+ ```
124
+
125
+ ## Testing
126
+
127
+ ```ruby
128
+ require "wayfinding/rspec"
129
+ ```
130
+
131
+ Each example runs inside `Wayfinding.preserve!`, so anything registered in a spec is rolled back. Use
132
+ `stub_destination` rather than mocking Wayfinding itself.
133
+
134
+ ```ruby
135
+ stub_destination(:home, "/homes/1")
136
+ stub_destination(:home) { |home| "/homes/#{home.id}" }
137
+ ```
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wayfinding
4
+ # A named, engine-scoped URL resolver with arbitrary application metadata.
5
+ # Metadata is stored without interpretation and read through #[]. Any field
6
+ # can be resolved lazily through #value_for.
7
+ class Destination
8
+ attr_reader :name, :kind, :attributes
9
+
10
+ # rubocop:disable Metrics/ParameterLists
11
+ def initialize(name:, engine: nil, kind: nil, helper: nil, resolver: nil, **attributes)
12
+ @name = name.to_sym
13
+ @engine = engine
14
+ @kind = kind&.to_sym
15
+ @helper = helper&.to_sym
16
+ @resolver = resolver
17
+ @attributes = attributes.freeze
18
+ freeze
19
+ end
20
+ # rubocop:enable Metrics/ParameterLists
21
+
22
+ def action = self[:action]
23
+
24
+ def subject = value_for(:subject)
25
+
26
+ def [](key) = attributes[key.to_sym]
27
+
28
+ def value_for(key) = resolve(self[key])
29
+
30
+ def path(*, **params)
31
+ return resolver_result(:path, *, **params) unless @helper
32
+
33
+ url_helpers.public_send(@helper, *, **params)
34
+ end
35
+
36
+ def url(*, **params)
37
+ return resolver_result(:url, *, **params) unless @helper
38
+
39
+ unless @helper.to_s.end_with?("_path")
40
+ raise Error, "#{name.inspect} declares helper #{@helper.inspect}, which does not end in `_path`"
41
+ end
42
+
43
+ url_helpers.public_send(@helper.to_s.sub(/_path\z/, "_url"), *, **params)
44
+ end
45
+
46
+ def engine
47
+ @engine.is_a?(Proc) ? @engine.call : @engine
48
+ end
49
+
50
+ private
51
+
52
+ def resolver_result(mode, *, **)
53
+ return @resolver.call(*, **) unless @engine
54
+
55
+ context = mode == :url ? UrlResolverContext.new(url_helpers) : url_helpers
56
+ context.instance_exec(*, **, &@resolver)
57
+ end
58
+
59
+ def url_helpers
60
+ engine.routes.url_helpers
61
+ end
62
+
63
+ def resolve(value)
64
+ value.respond_to?(:call) ? value.call : value
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wayfinding
4
+ # The result of Wayfinding.of_kind. An Array of Destination with ability filtering.
5
+ class DestinationList < Array
6
+ # Destinations the given ability permits. Destinations without an action are
7
+ # excluded: there is nothing to authorize against.
8
+ def accessible_by(ability)
9
+ self.class.new(select do |destination|
10
+ destination.action && ability.can?(destination.action, destination.subject)
11
+ end)
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wayfinding
4
+ Error = Class.new(StandardError)
5
+
6
+ # Raised when looking up a destination that was never registered.
7
+ UnregisteredDestination = Class.new(Error)
8
+
9
+ # Raised at registration when a destination is missing fields its kind requires,
10
+ # or when its resolution strategy is ambiguous or incomplete.
11
+ InvalidDestination = Class.new(Error)
12
+
13
+ # Raised when a destination declares a kind that was never defined.
14
+ UnknownKind = Class.new(Error)
15
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wayfinding
4
+ # A named field contract. Applications define kinds; Wayfinding only enforces them.
5
+ #
6
+ # Wayfinding.define_kind(:report, requires: %i[label description action subject])
7
+ #
8
+ # +requires+ may name any field, whether or not Wayfinding gives it behavior.
9
+ # Validation asserts presence only and never resolves callables, so a lazily
10
+ # registered <tt>subject: -> { ProjectTask }</tt> passes without autoloading.
11
+ class Kind
12
+ attr_reader :name, :requires
13
+
14
+ def initialize(name:, requires: [])
15
+ @name = name.to_sym
16
+ @requires = Array(requires).map(&:to_sym).freeze
17
+ freeze
18
+ end
19
+
20
+ def validate!(destination_name, given)
21
+ missing = requires.reject { |field| given.key?(field) && !given[field].nil? }
22
+ return if missing.empty?
23
+
24
+ raise InvalidDestination,
25
+ "Destination #{destination_name.inspect} of kind #{name.inspect} " \
26
+ "is missing required #{missing.length == 1 ? 'field' : 'fields'}: #{missing.join(', ')}"
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "wayfinding"
4
+
5
+ module Wayfinding
6
+ # Spec helpers for applications consuming Wayfinding.
7
+ #
8
+ # require "wayfinding/rspec"
9
+ #
10
+ # Each example runs inside Wayfinding.preserve!, so destinations registered in
11
+ # a spec are rolled back afterwards. Use #stub_destination rather than mocking
12
+ # Wayfinding itself.
13
+ module RSpecHelpers
14
+ # Register a destination that resolves to a fixed value, with no engine or
15
+ # route helpers involved.
16
+ #
17
+ # stub_destination(:home, "/homes/1")
18
+ # stub_destination(:home) { |home| "/homes/#{home.id}" }
19
+ def stub_destination(name, path = nil, **attributes, &resolver)
20
+ resolver ||= proc { |*, **| path }
21
+
22
+ Wayfinding.register(name: name, **attributes, &resolver)
23
+ end
24
+ end
25
+ end
26
+
27
+ RSpec.configure do |config|
28
+ config.include Wayfinding::RSpecHelpers
29
+
30
+ config.around do |example|
31
+ Wayfinding.preserve! { example.run }
32
+ end
33
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wayfinding
4
+ # Evaluates a destination block as a URL by delegating route helper calls to
5
+ # the registered engine and replacing a trailing `_path` with `_url`.
6
+ class UrlResolverContext
7
+ def initialize(url_helpers)
8
+ @url_helpers = url_helpers
9
+ end
10
+
11
+ private
12
+
13
+ def method_missing(name, ...)
14
+ @url_helpers.public_send(url_helper_name(name), ...)
15
+ end
16
+
17
+ def respond_to_missing?(name, include_private = false)
18
+ @url_helpers.respond_to?(url_helper_name(name), include_private)
19
+ end
20
+
21
+ def url_helper_name(name)
22
+ name.to_s.sub(/_path\z/, "_url")
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wayfinding
4
+ VERSION = "0.0.1"
5
+ end
data/lib/wayfinding.rb ADDED
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "wayfinding/errors"
4
+ require "wayfinding/kind"
5
+ require "wayfinding/url_resolver_context"
6
+ require "wayfinding/destination"
7
+ require "wayfinding/destination_list"
8
+ require "wayfinding/version"
9
+
10
+ # Cross-application URL resolution.
11
+ #
12
+ # Any component can link to any page without depending on the component that
13
+ # owns the route. Registered destinations are data, so this module's public
14
+ # surface never grows as destinations are added.
15
+ #
16
+ # Wayfinding.register(
17
+ # name: :home,
18
+ # engine: -> { Projects::Engine },
19
+ # helper: :home_path
20
+ # )
21
+ #
22
+ # Wayfinding.path_for(:home, home, current_tab: "Projects")
23
+ module Wayfinding
24
+ class << self
25
+ # Declare a named field contract. See Wayfinding::Kind.
26
+ def define_kind(name, requires: [])
27
+ kind = Kind.new(name: name, requires: requires)
28
+ kinds[kind.name] = kind
29
+ end
30
+
31
+ def kinds = @kinds ||= {}
32
+
33
+ def register(name:, engine: nil, kind: nil, helper: nil, **attributes, &resolver)
34
+ validate_resolution!(name, engine, helper, resolver)
35
+ validate_all_or_none!(name, attributes, %i[action subject])
36
+ validate_kind!(name, kind, **attributes)
37
+
38
+ destinations[name.to_sym] = Destination.new(
39
+ name: name, engine: engine, kind: kind, helper: helper, resolver: resolver, **attributes
40
+ )
41
+ end
42
+
43
+ def path_for(name, *, **params) = fetch(name).path(*, **params)
44
+
45
+ def url_for(name, *, **params) = fetch(name).url(*, **params)
46
+
47
+ def of_kind(name)
48
+ name = name.to_sym
49
+ DestinationList.new(destinations.each_value.select { |destination| destination.kind == name })
50
+ end
51
+
52
+ def fetch(name)
53
+ destinations.fetch(name.to_sym) do
54
+ raise UnregisteredDestination,
55
+ "No destination registered for #{name.inspect}. Registered: #{registered_names.join(', ')}"
56
+ end
57
+ end
58
+
59
+ def registered?(name) = destinations.key?(name.to_sym)
60
+
61
+ def registered_names = destinations.keys.sort
62
+
63
+ # Assert every expected destination is registered. Call at the end of an
64
+ # initializer so a dropped registration fails the boot rather than rendering
65
+ # a broken link.
66
+ def verify!(expected)
67
+ missing = Array(expected).map(&:to_sym) - registered_names
68
+ return true if missing.empty?
69
+
70
+ raise UnregisteredDestination, "Missing destination registrations: #{missing.join(', ')}"
71
+ end
72
+
73
+ # Snapshot the registry, yield, then restore it. Used by the RSpec helper so
74
+ # a destination registered inside an example does not leak.
75
+ def preserve!
76
+ destinations_snapshot = destinations.dup
77
+ kinds_snapshot = kinds.dup
78
+ yield
79
+ ensure
80
+ @destinations = destinations_snapshot
81
+ @kinds = kinds_snapshot
82
+ end
83
+
84
+ def reset!
85
+ @destinations = {}
86
+ @kinds = {}
87
+ end
88
+
89
+ private
90
+
91
+ def destinations = @destinations ||= {}
92
+
93
+ def validate_resolution!(name, engine, helper, resolver)
94
+ raise InvalidDestination, "#{name.inspect} declares both `helper:` and a block; use one" if helper && resolver
95
+ raise InvalidDestination, "#{name.inspect} declares neither `helper:` nor a block" unless helper || resolver
96
+ raise InvalidDestination, "#{name.inspect} declares `helper:` without an `engine:`" if helper && engine.nil?
97
+ end
98
+
99
+ def validate_all_or_none!(name, attributes, fields)
100
+ supplied = fields.reject { |field| attributes[field].nil? }
101
+ return if supplied.empty? || supplied.length == fields.length
102
+
103
+ missing = fields - supplied
104
+ raise InvalidDestination,
105
+ "#{name.inspect} must declare #{fields.map { |field| "`#{field}:`" }.join(' and ')} together; " \
106
+ "missing #{missing.map { |field| "`#{field}:`" }.join(', ')}"
107
+ end
108
+
109
+ def validate_kind!(name, kind, **given)
110
+ return if kind.nil?
111
+
112
+ definition = kinds.fetch(kind.to_sym) do
113
+ raise UnknownKind, "#{name.inspect} declares unknown kind #{kind.inspect}. Defined: #{kinds.keys.join(', ')}"
114
+ end
115
+
116
+ definition.validate!(name, given)
117
+ end
118
+ end
119
+ end
metadata ADDED
@@ -0,0 +1,192 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: wayfinding
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Nitro Developers
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: appraisal
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: 2.5.0
19
+ type: :development
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: 2.5.0
26
+ - !ruby/object:Gem::Dependency
27
+ name: bundler
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '2.1'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '2.1'
40
+ - !ruby/object:Gem::Dependency
41
+ name: license_finder
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '7.0'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '7.0'
54
+ - !ruby/object:Gem::Dependency
55
+ name: pry
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '0.14'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '0.14'
68
+ - !ruby/object:Gem::Dependency
69
+ name: pry-byebug
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - '='
73
+ - !ruby/object:Gem::Version
74
+ version: 3.10.1
75
+ type: :development
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - '='
80
+ - !ruby/object:Gem::Version
81
+ version: 3.10.1
82
+ - !ruby/object:Gem::Dependency
83
+ name: rainbow
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - '='
87
+ - !ruby/object:Gem::Version
88
+ version: 3.1.1
89
+ type: :development
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - '='
94
+ - !ruby/object:Gem::Version
95
+ version: 3.1.1
96
+ - !ruby/object:Gem::Dependency
97
+ name: rake
98
+ requirement: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - "~>"
101
+ - !ruby/object:Gem::Version
102
+ version: '13.0'
103
+ type: :development
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - "~>"
108
+ - !ruby/object:Gem::Version
109
+ version: '13.0'
110
+ - !ruby/object:Gem::Dependency
111
+ name: rspec
112
+ requirement: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - "~>"
115
+ - !ruby/object:Gem::Version
116
+ version: '3.0'
117
+ type: :development
118
+ prerelease: false
119
+ version_requirements: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - "~>"
122
+ - !ruby/object:Gem::Version
123
+ version: '3.0'
124
+ - !ruby/object:Gem::Dependency
125
+ name: simplecov
126
+ requirement: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - '='
129
+ - !ruby/object:Gem::Version
130
+ version: 0.15.1
131
+ type: :development
132
+ prerelease: false
133
+ version_requirements: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - '='
136
+ - !ruby/object:Gem::Version
137
+ version: 0.15.1
138
+ - !ruby/object:Gem::Dependency
139
+ name: yard
140
+ requirement: !ruby/object:Gem::Requirement
141
+ requirements:
142
+ - - '='
143
+ - !ruby/object:Gem::Version
144
+ version: 0.9.38
145
+ type: :development
146
+ prerelease: false
147
+ version_requirements: !ruby/object:Gem::Requirement
148
+ requirements:
149
+ - - '='
150
+ - !ruby/object:Gem::Version
151
+ version: 0.9.38
152
+ description: A global registry of named destinations, so any component can link to
153
+ any page without depending on the component that owns the route.
154
+ email:
155
+ - dev@powerhrg.com
156
+ executables: []
157
+ extensions: []
158
+ extra_rdoc_files: []
159
+ files:
160
+ - Rakefile
161
+ - docs/README.md
162
+ - lib/wayfinding.rb
163
+ - lib/wayfinding/destination.rb
164
+ - lib/wayfinding/destination_list.rb
165
+ - lib/wayfinding/errors.rb
166
+ - lib/wayfinding/kind.rb
167
+ - lib/wayfinding/rspec.rb
168
+ - lib/wayfinding/url_resolver_context.rb
169
+ - lib/wayfinding/version.rb
170
+ homepage: https://github.com/powerhome/power-tools
171
+ licenses:
172
+ - MIT
173
+ metadata:
174
+ rubygems_mfa_required: 'true'
175
+ rdoc_options: []
176
+ require_paths:
177
+ - lib
178
+ required_ruby_version: !ruby/object:Gem::Requirement
179
+ requirements:
180
+ - - ">="
181
+ - !ruby/object:Gem::Version
182
+ version: '3.2'
183
+ required_rubygems_version: !ruby/object:Gem::Requirement
184
+ requirements:
185
+ - - ">="
186
+ - !ruby/object:Gem::Version
187
+ version: '0'
188
+ requirements: []
189
+ rubygems_version: 4.0.3
190
+ specification_version: 4
191
+ summary: Cross-application URL resolution.
192
+ test_files: []