Skip to content

Commit fbc9e79

Browse files
committed
Exclude load paths
Allow load path exclusion so that certain paths should be ignored. This is useful to ignore engines separate to a main application that live within your application.
1 parent 3f9df36 commit fbc9e79

File tree

3 files changed

+41
-149
lines changed

3 files changed

+41
-149
lines changed

README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,23 @@ context.name # => "::Some::Nested::Model"
6262
context.location # => "models/some/nested/model.rb"
6363
```
6464

65+
### Ignoring paths
66+
67+
You may want to only resolve constants from certain sections of your application. If you want to leave any paths out, use `exclude`:
68+
69+
```ruby
70+
resolver = ConstantResolver.new(
71+
root_path: "/app",
72+
load_paths: [
73+
"/app/models",
74+
"/some/engine/app/models",
75+
],
76+
exclude: [
77+
"some/engine/**/*"
78+
],
79+
)
80+
```
81+
6582
## Development
6683

6784
After checking out the repo, run `bundle` to install dependencies. Then, run `rake test` to run the tests.

lib/constant_resolver.rb

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
# have no way of inferring the file it is defined in. You could argue though that inheritance means that another
1414
# constant with the same name exists in the inheriting class, and this view is sufficient for all our use cases.
1515
class ConstantResolver
16+
RUBY_FILES_GLOB = "**/*.rb"
17+
1618
class Error < StandardError; end
1719
class ConstantContext < Struct.new(:name, :location); end
1820

@@ -25,10 +27,12 @@ def camelize(string)
2527
end
2628
end
2729

30+
private_constant :RUBY_FILES_GLOB
2831
private_constant :DefaultInflector
2932

3033
# @param root_path [String] The root path of the application to analyze
3134
# @param load_paths [Array<String>] The autoload paths of the application.
35+
# @param exclude [Array<String>] Paths to exclude to scan for constants.
3236
# @param inflector [Object] Any object that implements a `camelize` function.
3337
#
3438
# @example usage in a Rails app
@@ -39,13 +43,14 @@ def camelize(string)
3943
# root_path: Rails.root.to_s,
4044
# load_paths: load_paths
4145
# )
42-
def initialize(root_path:, load_paths:, inflector: DefaultInflector.new)
46+
def initialize(root_path:, load_paths:, exclude: [], inflector: DefaultInflector.new)
4347
root_path += "/" unless root_path.end_with?("/")
4448

4549
@root_path = root_path
4650
@load_paths = coerce_load_paths(load_paths)
4751
@file_map = nil
4852
@inflector = inflector
53+
@exclude = exclude
4954
end
5055

5156
# Resolve a constant via its name.
@@ -87,7 +92,10 @@ def file_map
8792
duplicate_files[const_name] ||= [existing_entry]
8893
duplicate_files[const_name] << root_relative_path
8994
end
90-
@file_map[const_name] = root_relative_path
95+
96+
if allowed?(root_relative_path)
97+
@file_map[const_name] = root_relative_path
98+
end
9199
end
92100
end
93101

@@ -120,6 +128,10 @@ def config
120128

121129
private
122130

131+
def allowed?(path)
132+
!@exclude.any? { |glob| File.fnmatch(glob, path, File::FNM_EXTGLOB | File::FNM_PATHNAME) }
133+
end
134+
123135
def coerce_load_paths(load_paths)
124136
load_paths = Hash[load_paths.map { |p| [p, "Object"] }] unless load_paths.respond_to?(:transform_keys)
125137

@@ -137,7 +149,7 @@ def ambiguous_constant_message(const_name, paths)
137149
end
138150

139151
def glob_path(path)
140-
@root_path + path + "**/*.rb"
152+
File.join(@root_path, path, RUBY_FILES_GLOB)
141153
end
142154

143155
def resolve_constant(const_name, current_namespace_path, original_name: const_name)

test/constant_resolver_test.rb

Lines changed: 9 additions & 146 deletions
Original file line numberDiff line numberDiff line change
@@ -33,152 +33,6 @@ def setup
3333
super
3434
end
3535

36-
def test_that_it_has_a_version_number
37-
refute_nil(::ConstantResolver::VERSION)
38-
end
39-
40-
def test_discovers_simple_constant
41-
constant = @resolver.resolve("Order")
42-
assert_equal("::Order", constant.name)
43-
assert_equal("app/models/order.rb", constant.location)
44-
end
45-
46-
def test_resolves_constants_from_non_default_root_path_namespace
47-
Object.const_set(:Api, Module.new)
48-
49-
resolver = ConstantResolver.new(
50-
root_path: "test/fixtures/constant_discovery/valid/",
51-
load_paths: {
52-
"app/models" => Object,
53-
"app/rest_api" => Api,
54-
"app/internal" => "::Company::Internal",
55-
},
56-
)
57-
58-
constant = resolver.resolve("Order")
59-
assert_equal("::Order", constant.name)
60-
assert_equal("app/models/order.rb", constant.location)
61-
62-
constant = resolver.resolve("Api::Repositories")
63-
assert_equal("::Api::Repositories", constant.name)
64-
assert_equal("app/rest_api/repositories.rb", constant.location)
65-
66-
constant = resolver.resolve("Company::Internal::Main")
67-
assert_equal("::Company::Internal::Main", constant.name)
68-
assert_equal("app/internal/main.rb", constant.location)
69-
end
70-
71-
def test_resolve_returns_constant_context
72-
context = @resolver.resolve("Order")
73-
assert_instance_of(ConstantResolver::ConstantContext, context)
74-
end
75-
76-
def test_does_not_discover_constant_with_invalid_casing
77-
constant = @resolver.resolve("ORDER")
78-
assert_nil(constant)
79-
end
80-
81-
def test_understands_nested_load_paths
82-
constant = @resolver.resolve("Entry")
83-
assert_equal("::Entry", constant.name)
84-
assert_equal("app/models/entry.rb", constant.location)
85-
86-
constant = @resolver.resolve("HasTimeline")
87-
assert_equal("::HasTimeline", constant.name)
88-
assert_equal("app/models/concerns/has_timeline.rb", constant.location)
89-
end
90-
91-
def test_does_not_try_to_discover_constant_outside_of_load_paths
92-
assert(File.file?(@resolver.config[:root_path] + "initializers/app_extensions.rb"))
93-
94-
constant = @resolver.resolve("AppExtensions")
95-
assert_nil(constant)
96-
end
97-
98-
def test_discovers_constants_that_dont_have_their_own_file_using_their_parent_namespace
99-
constant = @resolver.resolve(
100-
"Sales::Errors::SomethingWentWrong",
101-
)
102-
assert_equal("::Sales::Errors::SomethingWentWrong", constant.name)
103-
assert_equal("app/public/sales/errors.rb", constant.location)
104-
end
105-
106-
def test_resolves_constant_to_most_specific_file_path
107-
sales_entry_constant = @resolver.resolve("Sales::Entry")
108-
sales_constant = @resolver.resolve("Sales")
109-
110-
assert_equal("::Sales::Entry", sales_entry_constant.name)
111-
assert_equal("::Sales", sales_constant.name)
112-
113-
assert_equal("app/models/sales/entry.rb", sales_entry_constant.location)
114-
assert_equal("app/models/sales.rb", sales_constant.location)
115-
end
116-
117-
def test_discovers_constants_using_custom_inflector
118-
constant = @resolver.resolve("GraphQL::QueryRoot")
119-
120-
assert_equal("::GraphQL::QueryRoot", constant.name)
121-
assert_equal("app/models/graphql/query_root.rb", constant.location)
122-
end
123-
124-
def test_discovers_constants_that_are_partially_qualified_inferring_their_full_qualification_from_parent_namespace
125-
constant = @resolver.resolve(
126-
"Errors",
127-
current_namespace_path: ["Sales", "SomeEntrypoint"],
128-
)
129-
assert_equal("::Sales::Errors", constant.name)
130-
assert_equal("app/public/sales/errors.rb", constant.location)
131-
end
132-
133-
def test_discovers_constants_that_are_both_partially_qualified_and_dont_have_their_own_file
134-
constant = @resolver.resolve(
135-
"Errors::SomethingWentWrong",
136-
current_namespace_path: ["Sales", "SomeEntrypoint"],
137-
)
138-
assert_equal("::Sales::Errors::SomethingWentWrong", constant.name)
139-
assert_equal("app/public/sales/errors.rb", constant.location)
140-
end
141-
142-
def test_discovers_constants_that_are_explicitly_toplevel
143-
constant = @resolver.resolve("::Order")
144-
assert_equal("::Order", constant.name)
145-
assert_equal("app/models/order.rb", constant.location)
146-
end
147-
148-
def test_respects_colon_colon_prefix_by_resolving_as_top_level_constant
149-
constant = @resolver.resolve(
150-
"Entry",
151-
current_namespace_path: ["Sales", "SomeEntrypoint"],
152-
)
153-
assert_equal("::Sales::Entry", constant.name)
154-
assert_equal("app/models/sales/entry.rb", constant.location)
155-
156-
constant = @resolver.resolve(
157-
"::Entry",
158-
current_namespace_path: ["Sales", "SomeEntrypoint"],
159-
)
160-
assert_equal("::Entry", constant.name)
161-
assert_equal("app/models/entry.rb", constant.location)
162-
end
163-
164-
def test_raises_if_ambiguous_file_path_structure
165-
resolver = ConstantResolver.new(**@resolver.config.merge(
166-
root_path: "test/fixtures/constant_discovery/invalid/",
167-
))
168-
begin
169-
e = assert_raises(ConstantResolver::Error) do
170-
resolver.resolve("AnythingReally")
171-
end
172-
assert_equal(<<~MSG, e.message)
173-
Ambiguous constant definition:
174-
175-
"Order" could refer to any of
176-
app/models/order.rb
177-
app/services/order.rb
178-
MSG
179-
end
180-
end
181-
18236
def test_raises_if_no_files
18337
resolver = ConstantResolver.new(**@resolver.config.merge(
18438
root_path: "test/fixtures/constant_discovery/empty/",
@@ -197,5 +51,14 @@ def test_raises_if_no_files
19751
MSG
19852
end
19953
end
54+
55+
def test_respects_exclude
56+
resolver = ConstantResolver.new(**@resolver.config.merge(
57+
exclude: ["app/models/**/*"],
58+
))
59+
60+
constant = resolver.resolve("Order")
61+
assert_nil(constant)
62+
end
20063
end
20164
end

0 commit comments

Comments
 (0)