Skip to content

Commit 7479883

Browse files
authored
Enhance Rails railtie to respect existing rails configuration (#478)
* Infer defaults from Rails' config * Use ActiveSupport.on_load hooks when setting Sitemap options * Allow setting config_file without using ENV vars * Allow lazily setting adapter class without forcing an eagerload
1 parent 7d30a9e commit 7479883

File tree

5 files changed

+266
-0
lines changed

5 files changed

+266
-0
lines changed
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
require 'spec_helper'
2+
3+
RSpec.describe "SitemapGenerator::Railtie" do
4+
let(:app) { Rails.application }
5+
let(:config) { app.config }
6+
let(:initializers) { app.initializers.index_by(&:name) }
7+
8+
it "adds a top-level configuration namespace" do
9+
expect(config.sitemap).to be_a ActiveSupport::OrderedOptions
10+
end
11+
12+
after { config.sitemap.clear }
13+
14+
describe "set_configs initializer" do
15+
subject(:initializer) { initializers["sitemap_generator.set_configs"] }
16+
17+
describe ".default_host" do
18+
after { app.routes.default_url_options = config.action_controller.default_url_options = {} }
19+
20+
it "ignores Rails if set directly" do
21+
app.routes.default_url_options = { host: "from_routes.test" }
22+
config.sitemap.default_host = "http://custom.test"
23+
24+
initializer.run(app)
25+
26+
expect(config.sitemap.default_host).to eq "http://custom.test"
27+
end
28+
29+
it "is inferred from Rails' routes default_url_options" do
30+
app.routes.default_url_options = { host: "from_routes.test" }
31+
32+
initializer.run(app)
33+
34+
expect(config.sitemap.default_host).to eq "http://from_routes.test"
35+
end
36+
37+
it "falls back to action_mailer, action_controller, and active_job" do
38+
config.action_controller.default_url_options = { host: "from_action_controller.test" }
39+
40+
initializer.run(app)
41+
42+
expect(config.sitemap.default_host).to eq "http://from_action_controller.test"
43+
end
44+
45+
it "doesn't construct a default_host if missing :host" do
46+
config.action_controller.default_url_options = { trailing_slash: true }
47+
48+
initializer.run(app)
49+
50+
expect(config.sitemap.default_host).to be_nil
51+
end
52+
end
53+
54+
describe ".sitemaps_host" do
55+
after { config.asset_host = config.action_controller.asset_host = nil }
56+
57+
it "can be set directly" do
58+
config.action_controller.asset_host = "http://from_action_controller.test"
59+
config.sitemap.sitemaps_host = "http://custom.test"
60+
61+
initializer.run(app)
62+
63+
expect(config.sitemap.sitemaps_host).to eq "http://custom.test"
64+
end
65+
66+
it "is inferred from action_controller/assets_host" do
67+
config.action_controller.asset_host = "http://from_action_controller.test"
68+
69+
initializer.run(app)
70+
71+
expect(config.sitemap.sitemaps_host).to eq "http://from_action_controller.test"
72+
end
73+
74+
it "doesn't accept procs" do
75+
config.action_controller.asset_host = -> { "dynamically construct hsot" }
76+
77+
initializer.run(app)
78+
79+
expect(config.sitemap.sitemaps_host).to be_nil
80+
end
81+
end
82+
83+
describe ".compress" do
84+
# config.assets provided by Propshaft or Sprockets
85+
before { config.assets = ActiveSupport::OrderedOptions[{gzip: true}] }
86+
after { config.assets = nil }
87+
88+
it "is inferred from config.assets.gzip" do
89+
initializer.run(app)
90+
91+
expect(config.sitemap.compress).to be true
92+
end
93+
94+
it "can be set directly (nil != false)" do
95+
config.sitemap.compress = false
96+
97+
initializer.run(app)
98+
99+
expect(config.sitemap.compress).to be false
100+
end
101+
end
102+
103+
describe ".public_path" do
104+
after { app.paths["public"] = "public" }
105+
106+
it "can be set directly" do
107+
config.sitemap.public_path = "custom"
108+
109+
initializer.run(app)
110+
111+
expect(config.sitemap.public_path).to eq "custom"
112+
end
113+
114+
it "is inferred from Rails paths" do
115+
app.paths["public"].unshift "inferred"
116+
117+
initializer.run(app)
118+
119+
expect(config.sitemap.public_path).to match "/inferred"
120+
end
121+
end
122+
end
123+
124+
describe "config_file initializer" do
125+
subject(:initializer) { initializers["sitemap_generator.config_file"] }
126+
127+
after { ENV.delete "CONFIG_FILE" }
128+
129+
it "sets CONFIG_FILE" do
130+
config.sitemap.config_file = "custom.rb"
131+
132+
expect { initializer.run(app) }
133+
.to change { ENV["CONFIG_FILE"] }.to("custom.rb")
134+
.and change(config, :sitemap).from have_key(:config_file)
135+
end
136+
137+
it "does not override CONFIG_FILE" do
138+
ENV["CONFIG_FILE"] = "existing.rb"
139+
config.sitemap.config_file = "override.rb"
140+
141+
expect { initializer.run(app) }
142+
.to_not change { ENV["CONFIG_FILE"] }.from("existing.rb")
143+
end
144+
end
145+
end

integration/spec/sitemap_generator/tasks_spec.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@ class << self
6969
end
7070
end
7171

72+
# This group of tests needs reworked.
73+
# They fail if rails app is reloaded or prepare hooks are re-run.
7274
describe "generate sitemap with normal config" do
7375
before :all do
7476
SitemapGenerator::Sitemap.reset!

lib/sitemap_generator/railtie.rb

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,60 @@
22

33
module SitemapGenerator
44
class Railtie < Rails::Railtie
5+
# Top level options object to namespace all settings
6+
config.sitemap = ActiveSupport::OrderedOptions.new
7+
58
rake_tasks do
69
load 'tasks/sitemap_generator_tasks.rake'
710
end
11+
12+
# Recognize existing Rails options as defaults for config.sitemap.*
13+
initializer 'sitemap_generator.set_configs' do |app|
14+
# routes.default_url_options takes precedence, falling back to configs
15+
url_opts = (app.default_url_options || {})
16+
.with_defaults(config.try(:action_controller).try(:default_url_options) || {})
17+
.with_defaults(config.try(:action_mailer).try(:default_url_options) || {})
18+
.with_defaults(config.try(:active_job).try(:default_url_options) || {})
19+
20+
config.sitemap.default_host ||= ActionDispatch::Http::URL.full_url_for(url_opts) if url_opts.key?(:host)
21+
22+
# Rails defaults action_controller.asset_host and action_mailer.asset_host
23+
# to the top-level config.asset_host so we get that for free here.
24+
config.sitemap.sitemaps_host ||= [
25+
config.try(:action_controller).try(:asset_host),
26+
config.try(:action_mailer).try(:asset_host)
27+
].grep(String).first
28+
29+
config.sitemap.compress = config.try(:assets).try(:gzip) if config.sitemap.compress.nil?
30+
31+
config.sitemap.public_path ||= app.paths['public'].first
32+
33+
# "Compile" config.sitemap options onto the Sitemap class.
34+
config.after_initialize do
35+
ActiveSupport.on_load(:sitemap_generator, yield: true) do |sitemap|
36+
config.sitemap.except(:adapter).each { |k, v| sitemap.public_send("#{k}=", v) }
37+
end
38+
end
39+
end
40+
41+
# Allow setting the CONFIG_FILE without relying on env var;
42+
# (e.g in config/application or environments/*.rb)
43+
initializer 'sitemap_generator.config_file' do
44+
if (config_file = config.sitemap.delete(:config_file).presence) && ENV['CONFIG_FILE'].blank?
45+
ENV['CONFIG_FILE'] = config_file
46+
end
47+
end
48+
49+
# Allow lazily setting the adapter class without forcing an autoload.
50+
# (ie. string or symbol name; or Callable (proc/lambda/etc))
51+
initializer 'sitemap_generator.adapter' do |app|
52+
config.to_prepare do
53+
ActiveSupport.on_load(:sitemap_generator) do
54+
self.adapter = Utilities.find_adapter app.config.sitemap.adapter
55+
end
56+
end
57+
end
858
end
59+
60+
ActiveSupport.run_load_hooks(:sitemap_generator, Sitemap)
961
end

lib/sitemap_generator/utilities.rb

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,5 +180,29 @@ def ellipsis(string, max)
180180
def bytesize(string)
181181
string.respond_to?(:bytesize) ? string.bytesize : string.length
182182
end
183+
184+
# Looks up an adapter from various sources:
185+
# 1. symbol/string names are camelized and constantized
186+
# - :fog becomes SitemapGenerator::FogAdapter
187+
# - 'external/adapter_class' becomes External::AdapterClass
188+
# 2. classes are instantiated
189+
# 3. callables (procs, lambdas, #call) are "called"
190+
# All steps recurse, so a proc could return a string that names a class that is instantiated.
191+
#
192+
# This method is used by the Rails railtie and as such,
193+
# safely depends on ActiveSupport::Inflector.
194+
def find_adapter(candidate)
195+
if candidate.is_a?(Symbol) || candidate.is_a?(String)
196+
candidate.to_s.camelize.then { |name|
197+
"SitemapGenerator::#{name}Adapter".safe_constantize || name.safe_constantize
198+
}.then { |c| find_adapter(c) }
199+
elsif candidate.respond_to?(:new)
200+
find_adapter candidate.new
201+
elsif candidate.respond_to?(:call)
202+
find_adapter candidate.call
203+
else
204+
candidate
205+
end
206+
end
183207
end
184208
end

spec/sitemap_generator/utilities_spec.rb

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,4 +100,47 @@
100100
expect(SitemapGenerator::Utilities.ellipsis('aaa', 1)).to eq('...')
101101
end
102102
end
103+
104+
describe 'find_adapter' do
105+
require 'active_support/core_ext/string/inflections'
106+
107+
context 'when candidate is an adapter (#write)' do
108+
let(:candidate) { SitemapGenerator::FileAdapter.new }
109+
it "returns the same candidate" do
110+
expect(subject.find_adapter candidate).to be candidate
111+
end
112+
end
113+
114+
context 'when candidate is a callable (proc, lambda, #call)' do
115+
it "invokes call and returns that" do
116+
expect(subject.find_adapter -> { SitemapGenerator::FileAdapter.new }).to be_a SitemapGenerator::FileAdapter
117+
end
118+
119+
it "recurses on the return" do
120+
expect(subject.find_adapter -> { SitemapGenerator::FileAdapter }).to be_a SitemapGenerator::FileAdapter
121+
expect(subject.find_adapter -> { :file }).to be_a SitemapGenerator::FileAdapter
122+
end
123+
end
124+
125+
context 'when candidate is a class (#new)' do
126+
it "instantiates the class" do
127+
expect(subject.find_adapter SitemapGenerator::FileAdapter).to be_a SitemapGenerator::FileAdapter
128+
end
129+
end
130+
131+
context 'when candidate is a class name' do
132+
it "constantizes" do
133+
expect(subject.find_adapter :file).to be_a SitemapGenerator::FileAdapter
134+
expect(subject.find_adapter "file").to be_a SitemapGenerator::FileAdapter
135+
expect(subject.find_adapter 'sitemap_generator/file_adapter').to be_a SitemapGenerator::FileAdapter
136+
end
137+
end
138+
139+
context 'when candidate is anything else' do
140+
let(:candidate) { double }
141+
it "returns the same candidate" do
142+
expect(subject.find_adapter candidate).to be candidate
143+
end
144+
end
145+
end
103146
end

0 commit comments

Comments
 (0)