Skip to content

Commit 5295b32

Browse files
authored
MONGOID-5555 Avoid duplicating option documentation (#5539)
* Add config introspection to avoid duplicating docs in mongoid.yml template * update app_spec to look for config options in mongoid.yml * prefer expectation syntax for consistency's sake * avoid the eval * test the full comment, and every config option, for presence in the config file * don't need to call strip twice
1 parent cb7e0bd commit 5295b32

File tree

5 files changed

+282
-55
lines changed

5 files changed

+282
-55
lines changed

lib/mongoid/config.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
require "mongoid/config/environment"
55
require "mongoid/config/options"
66
require "mongoid/config/validators"
7+
require "mongoid/config/introspection"
78

89
module Mongoid
910

lib/mongoid/config/introspection.rb

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
# frozen_string_literal: true
2+
3+
module Mongoid
4+
module Config
5+
6+
# This module provides a way to inspect not only the defined configuration
7+
# settings and their defaults (which are available via
8+
# `Mongoid::Config.settings`), but also the documentation about them. It
9+
# does this by scraping the `mongoid/config.rb` file with a regular
10+
# expression to match comments with options.
11+
#
12+
# @api private
13+
module Introspection
14+
extend self
15+
16+
# A helper class to represent an individual option, its name, its
17+
# default value, and the comment that documents it.
18+
class Option
19+
# The name of this option.
20+
#
21+
# @return [ String ] The name of the option
22+
attr_reader :name
23+
24+
# The default value of this option.
25+
#
26+
# @return [ Object ] The default value of the option, typically a
27+
# String, Symbol, nil, true, or false.
28+
attr_reader :default
29+
30+
# The comment that describes this option, as scraped from
31+
# mongoid/config.rb.
32+
#
33+
# @return [ String ] The (possibly multi-line) comment. Each line is
34+
# prefixed with the Ruby comment character ("#").
35+
attr_reader :comment
36+
37+
# Instantiate an option from an array of Regex captures.
38+
#
39+
# @param [ Array<String> ] captures The array with the Regex captures
40+
# to use to instantiate the option. The element at index 1 must be
41+
# the comment, at index 2 must be the name, and at index 3 must be
42+
# the default value.
43+
#
44+
# @return [ Option ] The newly instantiated Option object.
45+
def self.from_captures(captures)
46+
new(captures[2], captures[3], captures[1])
47+
end
48+
49+
# Create a new Option instance with the given name, default value,
50+
# and comment.
51+
#
52+
# @param [ String ] name The option's name.
53+
# @param [ String ] default The option's default value, as a String
54+
# representing the actual Ruby value.
55+
# @param [ String ] comment The multi-line comment describing the
56+
# option.
57+
def initialize(name, default, comment)
58+
@name, @default, @comment = name, default, unindent(comment)
59+
end
60+
61+
# Indent the comment by the requested amount, optionally indenting the
62+
# first line, as well.
63+
#
64+
# param [ Integer ] indent The number of spaces to indent each line
65+
# (Default: 2)
66+
# param [ true | false ] indent_first_line Whether or not to indent
67+
# the first line of the comment (Default: false)
68+
#
69+
# @return [ String ] the reformatted comment
70+
def indented_comment(indent: 2, indent_first_line: false)
71+
comment.gsub(/^/, " " * indent).tap do |result|
72+
result.strip! unless indent_first_line
73+
end
74+
end
75+
76+
# Reports whether or not the text "(Deprecated)" is present in the
77+
# option's comment.
78+
#
79+
# @return [ true | false ] whether the option is deprecated or not.
80+
def deprecated?
81+
comment.include?("(Deprecated)")
82+
end
83+
84+
# Compare self with the given option.
85+
#
86+
# @return [ true | false ] If name, default, and comment are all the
87+
# same, return true. Otherwise, false.
88+
def ==(option)
89+
name == option.name &&
90+
default == option.default &&
91+
comment == option.comment
92+
end
93+
94+
private
95+
96+
# Removes any existing whitespace from the beginning of each line in
97+
# the text.
98+
#
99+
# @param [ String ] text The text to unindent.
100+
#
101+
# @return [ String ] the unindented text.
102+
def unindent(text)
103+
text.strip.gsub(/^\s+/, "")
104+
end
105+
end
106+
107+
# A regular expression that looks for option declarations of the format:
108+
#
109+
# # one or more lines of comments,
110+
# # followed immediately by an option
111+
# # declaration with a default value:
112+
# option :option_name, default: "something"
113+
#
114+
# The regex produces three captures:
115+
#
116+
# 1: the (potentially multiline) comment
117+
# 2: the option's name
118+
# 3: the option's default value
119+
OPTION_PATTERN = %r{
120+
(
121+
((?:^\s*\#.*\n)+) # match one or more lines of comments
122+
^\s+option\s+ # followed immediately by a line declaring an option
123+
:(\w+),\s+ # match the option's name, followed by a comma
124+
default:\s+(.*) # match the default value for the option
125+
\n) # end with a newline
126+
}x
127+
128+
# The full path to the source file of the Mongoid::Config module.
129+
CONFIG_RB_PATH = File.absolute_path(File.join(
130+
File.dirname(__FILE__), "../config.rb"))
131+
132+
# Extracts the available configuration options from the Mongoid::Config
133+
# source file, and returns them as an array of hashes.
134+
#
135+
# @param [ true | false ] include_deprecated Whether deprecated options
136+
# should be included in the list. (Default: false)
137+
#
138+
# @return [ Array<Introspection::Option>> ] the array of option objects
139+
# representing each defined option, in alphabetical order by name.
140+
def options(include_deprecated: false)
141+
src = File.read(CONFIG_RB_PATH)
142+
src.scan(OPTION_PATTERN)
143+
.map { |opt| Option.from_captures(opt) }
144+
.reject { |opt| !include_deprecated && opt.deprecated? }
145+
.sort_by { |opt| opt.name }
146+
end
147+
end
148+
149+
end
150+
end

lib/rails/generators/mongoid/config/templates/mongoid.yml

Lines changed: 5 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -119,59 +119,11 @@ development:
119119

120120
# Configure Mongoid specific options. (optional)
121121
options:
122-
# Application name that is printed to the mongodb logs upon establishing
123-
# a connection in server versions >= 3.4. Note that the name cannot
124-
# exceed 128 bytes. It is also used as the database name if the
125-
# database name is not explicitly defined. (default: nil)
126-
# app_name: MyApplicationName
127-
128-
# Mark belongs_to associations as required by default, so that saving a
129-
# model with a missing belongs_to association will trigger a validation
130-
# error. (default: true)
131-
# belongs_to_required_by_default: true
132-
133-
# Raise an exception when a field is redefined. (default: false)
134-
# duplicate_fields_exception: false
135-
136-
# Include the root model name in json serialization. (default: false)
137-
# include_root_in_json: false
138-
139-
# Include the _type field in serialization. (default: false)
140-
# include_type_for_serialization: false
141-
142-
# Whether to join nested persistence contexts for atomic operations
143-
# to parent contexts by default. (default: false)
144-
# join_contexts: false
145-
146-
# Set the Mongoid and Ruby driver log levels when Mongoid is not using
147-
# Ruby on Rails logger instance. (default: :info)
148-
# log_level: :info
149-
150-
# Preload all models in development, needed when models use
151-
# inheritance. (default: false)
152-
# preload_models: false
153-
154-
# Raise an error when performing a #find and the document is not found.
155-
# (default: true)
156-
# raise_not_found_error: true
157-
158-
# Raise an error when defining a scope with the same name as an
159-
# existing method. (default: false)
160-
# scope_overwrite_exception: false
161-
162-
# Use ActiveSupport's time zone in time operations instead of
163-
# the Ruby default time zone. See the time zone section below for
164-
# further information. (default: true)
165-
# use_activesupport_time_zone: true
166-
167-
# Return stored times as UTC. See the time zone section below for
168-
# further information. Most applications should not use this option.
169-
# (default: false)
170-
# use_utc: false
171-
172-
# (Deprecated) In MongoDB 4.0 and earlier, set whether to create
173-
# indexes in the background by default. (default: false)
174-
# background_indexing: false
122+
<%- Mongoid::Config::Introspection.options.each do |opt| -%>
123+
<%= opt.indented_comment(indent: 4) %>
124+
# <%= opt.name %>: <%= opt.default %>
125+
126+
<%- end -%>
175127

176128
test:
177129
clients:

spec/integration/app_spec.rb

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,8 +125,19 @@ def prepare_new_rails_app(name)
125125
File.exist?(mongoid_config_file).should be true
126126

127127
config_text = File.read(mongoid_config_file)
128-
config_text.should =~ /mongoid_test_config_development/
129-
config_text.should =~ /mongoid_test_config_test/
128+
expect(config_text).to match /mongoid_test_config_development/
129+
expect(config_text).to match /mongoid_test_config_test/
130+
131+
Mongoid::Config::Introspection.options(include_deprecated: true).each do |opt|
132+
if opt.deprecated?
133+
# deprecated options should not be included
134+
expect(config_text).not_to include "# #{opt.name}:"
135+
else
136+
block = " #{opt.indented_comment(indent: 4)}\n" \
137+
" # #{opt.name}: #{opt.default}\n"
138+
expect(config_text).to include block
139+
end
140+
end
130141
end
131142
end
132143

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
# frozen_string_literal: true
2+
3+
require "spec_helper"
4+
5+
describe Mongoid::Config::Introspection do
6+
7+
context "CONFIG_RB_PATH" do
8+
it "refers to an actual source file" do
9+
expect(File.exist?(Mongoid::Config::Introspection::CONFIG_RB_PATH)).to be true
10+
end
11+
end
12+
13+
context "#options" do
14+
let(:options) { Mongoid::Config::Introspection.options }
15+
let(:all_options) { Mongoid::Config::Introspection.options(include_deprecated: true) }
16+
let(:deprecated_options) { all_options.select(&:deprecated?) }
17+
18+
# NOTE: the "with deprecated options" context tests a configuration option
19+
# that is known to be deprecated (see `let(:option_name)`). When this
20+
# deprecated option is eventually removed, the "exclude" spec will break.
21+
# At that time, update the `:option_name` helper to reference a different
22+
# deprecated option (if any), or else skip the specs in this context.
23+
context "with deprecated options" do
24+
let(:option_name) { "background_indexing" }
25+
26+
it "should exclude deprecated options by default" do
27+
option = options.detect { |opt| opt.name == option_name }
28+
expect(option).to be_nil
29+
end
30+
31+
it "deprecated options should be included when requested" do
32+
option = all_options.detect { |opt| opt.name == option_name }
33+
expect(option).not_to be_nil
34+
end
35+
end
36+
37+
Mongoid::Config.defaults.each do |name, default_value|
38+
context "for the `#{name}` option" do
39+
let(:option) { all_options.detect { |opt| opt.name == name.to_s } }
40+
let(:live_option) { options.detect { |opt| opt.name == name.to_s } }
41+
42+
it "should be parsed by the introspection scraper" do
43+
expect(option).not_to be_nil
44+
expect(option.default).to be == default_value.inspect
45+
expect(option.comment.strip).not_to be_empty
46+
end
47+
48+
it "should be excluded by default if it is deprecated" do
49+
if option.deprecated?
50+
expect(live_option).to be_nil
51+
else
52+
expect(live_option).to be == option
53+
end
54+
end
55+
end
56+
end
57+
end
58+
59+
describe Mongoid::Config::Introspection::Option do
60+
let(:option) do
61+
Mongoid::Config::Introspection::Option.new(
62+
"name", "default", " # line 1\n # line 2\n")
63+
end
64+
65+
context ".from_captures" do
66+
it "populates the option's fields" do
67+
option = Mongoid::Config::Introspection::Option.from_captures([nil, "# comment", "name", "default"])
68+
expect(option.name).to be == "name"
69+
expect(option.default).to be == "default"
70+
expect(option.comment).to be == "# comment"
71+
end
72+
end
73+
74+
context "#initialize" do
75+
it "unindents the given comment" do
76+
expect(option.name).to be == "name"
77+
expect(option.default).to be == "default"
78+
expect(option.comment).to be == "# line 1\n# line 2"
79+
end
80+
end
81+
82+
context "#indented_comment" do
83+
it "has defaults" do
84+
expect(option.indented_comment).to be == "# line 1\n # line 2"
85+
end
86+
87+
it "allows indentation to be specified" do
88+
expect(option.indented_comment(indent: 3)).to be == "# line 1\n # line 2"
89+
end
90+
91+
it "allows the first line to be indented" do
92+
expect(option.indented_comment(indent: 3, indent_first_line: true))
93+
.to be == " # line 1\n # line 2"
94+
end
95+
end
96+
97+
context "#deprecated?" do
98+
let(:deprecated_option) do
99+
Mongoid::Config::Introspection::Option.new(
100+
"name", "default", "# this\n# is (Deprecated), yo\n")
101+
end
102+
103+
it "is not deprecated by default" do
104+
expect(option.deprecated?).not_to be true
105+
end
106+
107+
it "is deprecated when the comment includes \"(Deprecated)\"" do
108+
expect(deprecated_option.deprecated?).to be true
109+
end
110+
end
111+
end
112+
113+
end

0 commit comments

Comments
 (0)