brainstem 1.0.0.pre.1 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +12 -0
- data/Gemfile.lock +1 -1
- data/README.md +383 -32
- data/bin/brainstem +6 -0
- data/brainstem.gemspec +2 -0
- data/docs/api_doc_generator.markdown +175 -0
- data/docs/brainstem_executable.markdown +32 -0
- data/docs/docgen.png +0 -0
- data/docs/docgen_ascii.txt +63 -0
- data/docs/executable.png +0 -0
- data/docs/executable_ascii.txt +10 -0
- data/lib/brainstem/api_docs.rb +146 -0
- data/lib/brainstem/api_docs/abstract_collection.rb +116 -0
- data/lib/brainstem/api_docs/atlas.rb +158 -0
- data/lib/brainstem/api_docs/builder.rb +167 -0
- data/lib/brainstem/api_docs/controller.rb +122 -0
- data/lib/brainstem/api_docs/controller_collection.rb +40 -0
- data/lib/brainstem/api_docs/endpoint.rb +234 -0
- data/lib/brainstem/api_docs/endpoint_collection.rb +58 -0
- data/lib/brainstem/api_docs/exceptions.rb +8 -0
- data/lib/brainstem/api_docs/formatters/abstract_formatter.rb +64 -0
- data/lib/brainstem/api_docs/formatters/markdown/controller_formatter.rb +76 -0
- data/lib/brainstem/api_docs/formatters/markdown/endpoint_collection_formatter.rb +73 -0
- data/lib/brainstem/api_docs/formatters/markdown/endpoint_formatter.rb +169 -0
- data/lib/brainstem/api_docs/formatters/markdown/helper.rb +76 -0
- data/lib/brainstem/api_docs/formatters/markdown/presenter_formatter.rb +200 -0
- data/lib/brainstem/api_docs/introspectors/abstract_introspector.rb +100 -0
- data/lib/brainstem/api_docs/introspectors/rails_introspector.rb +232 -0
- data/lib/brainstem/api_docs/presenter.rb +225 -0
- data/lib/brainstem/api_docs/presenter_collection.rb +97 -0
- data/lib/brainstem/api_docs/resolver.rb +73 -0
- data/lib/brainstem/api_docs/sinks/abstract_sink.rb +37 -0
- data/lib/brainstem/api_docs/sinks/controller_presenter_multifile_sink.rb +93 -0
- data/lib/brainstem/api_docs/sinks/stdout_sink.rb +44 -0
- data/lib/brainstem/cli.rb +146 -0
- data/lib/brainstem/cli/abstract_command.rb +97 -0
- data/lib/brainstem/cli/generate_api_docs_command.rb +169 -0
- data/lib/brainstem/concerns/controller_dsl.rb +300 -0
- data/lib/brainstem/concerns/controller_param_management.rb +30 -9
- data/lib/brainstem/concerns/formattable.rb +38 -0
- data/lib/brainstem/concerns/inheritable_configuration.rb +3 -2
- data/lib/brainstem/concerns/optional.rb +43 -0
- data/lib/brainstem/concerns/presenter_dsl.rb +76 -15
- data/lib/brainstem/controller_methods.rb +6 -3
- data/lib/brainstem/dsl/association.rb +6 -3
- data/lib/brainstem/dsl/associations_block.rb +6 -3
- data/lib/brainstem/dsl/base_block.rb +2 -4
- data/lib/brainstem/dsl/conditional.rb +7 -3
- data/lib/brainstem/dsl/conditionals_block.rb +4 -4
- data/lib/brainstem/dsl/configuration.rb +184 -8
- data/lib/brainstem/dsl/field.rb +6 -3
- data/lib/brainstem/dsl/fields_block.rb +2 -3
- data/lib/brainstem/help_text.txt +8 -0
- data/lib/brainstem/presenter.rb +27 -6
- data/lib/brainstem/presenter_validator.rb +5 -2
- data/lib/brainstem/time_classes.rb +1 -1
- data/lib/brainstem/version.rb +1 -1
- data/spec/brainstem/api_docs/abstract_collection_spec.rb +156 -0
- data/spec/brainstem/api_docs/atlas_spec.rb +353 -0
- data/spec/brainstem/api_docs/builder_spec.rb +100 -0
- data/spec/brainstem/api_docs/controller_collection_spec.rb +92 -0
- data/spec/brainstem/api_docs/controller_spec.rb +225 -0
- data/spec/brainstem/api_docs/endpoint_collection_spec.rb +144 -0
- data/spec/brainstem/api_docs/endpoint_spec.rb +346 -0
- data/spec/brainstem/api_docs/formatters/abstract_formatter_spec.rb +30 -0
- data/spec/brainstem/api_docs/formatters/markdown/controller_formatter_spec.rb +126 -0
- data/spec/brainstem/api_docs/formatters/markdown/endpoint_collection_formatter_spec.rb +85 -0
- data/spec/brainstem/api_docs/formatters/markdown/endpoint_formatter_spec.rb +261 -0
- data/spec/brainstem/api_docs/formatters/markdown/helper_spec.rb +100 -0
- data/spec/brainstem/api_docs/formatters/markdown/presenter_formatter_spec.rb +485 -0
- data/spec/brainstem/api_docs/introspectors/abstract_introspector_spec.rb +192 -0
- data/spec/brainstem/api_docs/introspectors/rails_introspector_spec.rb +170 -0
- data/spec/brainstem/api_docs/presenter_collection_spec.rb +84 -0
- data/spec/brainstem/api_docs/presenter_spec.rb +519 -0
- data/spec/brainstem/api_docs/resolver_spec.rb +72 -0
- data/spec/brainstem/api_docs/sinks/abstract_sink_spec.rb +16 -0
- data/spec/brainstem/api_docs/sinks/controller_presenter_multifile_sink_spec.rb +56 -0
- data/spec/brainstem/api_docs/sinks/stdout_sink_spec.rb +22 -0
- data/spec/brainstem/api_docs_spec.rb +58 -0
- data/spec/brainstem/cli/abstract_command_spec.rb +91 -0
- data/spec/brainstem/cli/generate_api_docs_command_spec.rb +125 -0
- data/spec/brainstem/cli_spec.rb +67 -0
- data/spec/brainstem/concerns/controller_dsl_spec.rb +471 -0
- data/spec/brainstem/concerns/controller_param_management_spec.rb +36 -16
- data/spec/brainstem/concerns/formattable_spec.rb +30 -0
- data/spec/brainstem/concerns/inheritable_configuration_spec.rb +104 -4
- data/spec/brainstem/concerns/optional_spec.rb +48 -0
- data/spec/brainstem/concerns/presenter_dsl_spec.rb +202 -31
- data/spec/brainstem/dsl/association_spec.rb +18 -2
- data/spec/brainstem/dsl/conditional_spec.rb +25 -2
- data/spec/brainstem/dsl/configuration_spec.rb +1 -1
- data/spec/brainstem/dsl/field_spec.rb +18 -2
- data/spec/brainstem/presenter_collection_spec.rb +10 -2
- data/spec/brainstem/presenter_spec.rb +32 -0
- data/spec/brainstem/presenter_validator_spec.rb +12 -7
- data/spec/dummy/rails.rb +49 -0
- data/spec/shared/atlas_taker.rb +18 -0
- data/spec/shared/formattable.rb +14 -0
- data/spec/spec_helper.rb +2 -0
- data/spec/spec_helpers/db.rb +1 -1
- data/spec/spec_helpers/presenters.rb +20 -14
- metadata +106 -6
@@ -1,22 +1,43 @@
|
|
1
|
-
|
1
|
+
require 'active_support/concern'
|
2
|
+
require 'active_support/inflector'
|
3
|
+
|
4
|
+
# Provide `brainstem_model_name` and `brainstem_plural_model_name` in
|
5
|
+
# controllers for use when accessing the `params` hash.
|
2
6
|
|
3
7
|
module Brainstem
|
4
8
|
module Concerns
|
5
9
|
module ControllerParamManagement
|
6
10
|
extend ActiveSupport::Concern
|
7
11
|
|
8
|
-
included do
|
9
|
-
class_attribute :brainstem_plural_model_name, :brainstem_model_name,
|
10
|
-
instance_accessor: false, instance_reader: false, instance_writer: false
|
11
|
-
end
|
12
|
-
|
13
12
|
def brainstem_model_name
|
14
|
-
self.class.brainstem_model_name.to_s
|
13
|
+
self.class.brainstem_model_name.to_s
|
15
14
|
end
|
16
15
|
|
17
16
|
def brainstem_plural_model_name
|
18
|
-
self.class.brainstem_plural_model_name.to_s
|
17
|
+
self.class.brainstem_plural_model_name.to_s
|
18
|
+
end
|
19
|
+
|
20
|
+
module ClassMethods
|
21
|
+
def brainstem_model_name
|
22
|
+
@brainstem_model_name ||= controller_name.singularize
|
23
|
+
end
|
24
|
+
|
25
|
+
def brainstem_plural_model_name
|
26
|
+
@brainstem_plural_model_name ||= self.brainstem_model_name.pluralize
|
27
|
+
end
|
28
|
+
|
29
|
+
def brainstem_model_name=(name)
|
30
|
+
@brainstem_model_name = name
|
31
|
+
end
|
32
|
+
|
33
|
+
def brainstem_plural_model_name=(name)
|
34
|
+
@brainstem_plural_model_name = name
|
35
|
+
end
|
36
|
+
|
37
|
+
def brainstem_model_class
|
38
|
+
@brainstem_model_class ||= self.brainstem_model_name.classify.constantize
|
39
|
+
end
|
19
40
|
end
|
20
41
|
end
|
21
42
|
end
|
22
|
-
end
|
43
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'brainstem/api_docs'
|
2
|
+
require 'active_support/inflector/inflections'
|
3
|
+
|
4
|
+
|
5
|
+
module Brainstem
|
6
|
+
module Concerns
|
7
|
+
module Formattable
|
8
|
+
attr_writer :formatters
|
9
|
+
|
10
|
+
|
11
|
+
def valid_options
|
12
|
+
super | [ :formatters ]
|
13
|
+
end
|
14
|
+
|
15
|
+
|
16
|
+
def formatters
|
17
|
+
@formatters ||= ::Brainstem::ApiDocs::FORMATTERS[formatter_type]
|
18
|
+
end
|
19
|
+
|
20
|
+
|
21
|
+
def formatted_as(format, options = {})
|
22
|
+
formatters[format].call(self, options)
|
23
|
+
end
|
24
|
+
|
25
|
+
|
26
|
+
#
|
27
|
+
# Declares the type of formatter that should be used to format an entity
|
28
|
+
# of this class.
|
29
|
+
#
|
30
|
+
def formatter_type
|
31
|
+
self.class.to_s
|
32
|
+
.demodulize
|
33
|
+
.underscore
|
34
|
+
.to_sym
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -1,10 +1,11 @@
|
|
1
|
+
require 'active_support/concern'
|
1
2
|
require 'brainstem/dsl/configuration'
|
2
3
|
|
3
4
|
module Brainstem
|
4
5
|
module Concerns
|
5
6
|
module InheritableConfiguration
|
6
7
|
extend ActiveSupport::Concern
|
7
|
-
|
8
|
+
|
8
9
|
module ClassMethods
|
9
10
|
def configuration
|
10
11
|
@configuration ||= begin
|
@@ -26,4 +27,4 @@ module Brainstem
|
|
26
27
|
end
|
27
28
|
end
|
28
29
|
end
|
29
|
-
end
|
30
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
#
|
2
|
+
# This module is used in lieu of an dependency on optional kwargs. Any symbol
|
3
|
+
# included in the array of +#valid_options+ will be whitelisted from passed
|
4
|
+
# options and sent to the instance on initialization.
|
5
|
+
#
|
6
|
+
# In this simple way, we can make classes accept options, whitelist which
|
7
|
+
# are acceptable, and set them without having to manually extend our
|
8
|
+
# initializer.
|
9
|
+
#
|
10
|
+
# In order to use this, your constructor must have an argument named +options+
|
11
|
+
# that defaults to an empty hash. Additionally, for each option you define, you
|
12
|
+
# should define an accessor or at least a writer.
|
13
|
+
#
|
14
|
+
# You may also implement a +#valid_options+ method. It is recommended that you
|
15
|
+
# make this the union of the superclass's +valid_options+ method and this
|
16
|
+
# class's options so that inherited options are preserved:
|
17
|
+
#
|
18
|
+
# @example
|
19
|
+
# def valid_options
|
20
|
+
# super | [ :your_options_here ]
|
21
|
+
# end
|
22
|
+
#
|
23
|
+
module Brainstem
|
24
|
+
module Concerns
|
25
|
+
module Optional
|
26
|
+
|
27
|
+
#
|
28
|
+
# The options that should be extracted and sent to the class on
|
29
|
+
# initialization.
|
30
|
+
#
|
31
|
+
# @return [Array<Symbol>] valid options
|
32
|
+
#
|
33
|
+
def valid_options
|
34
|
+
[ ]
|
35
|
+
end
|
36
|
+
|
37
|
+
|
38
|
+
def initialize(options = {})
|
39
|
+
options.slice(*valid_options).each {|k, v| self.send("#{k}=", v) }
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -9,6 +9,8 @@ require 'brainstem/dsl/fields_block'
|
|
9
9
|
require 'brainstem/dsl/associations_block'
|
10
10
|
|
11
11
|
|
12
|
+
require 'active_support/core_ext/array/extract_options'
|
13
|
+
|
12
14
|
module Brainstem
|
13
15
|
module Concerns
|
14
16
|
module PresenterDSL
|
@@ -36,6 +38,18 @@ module Brainstem
|
|
36
38
|
AssociationsBlock.new(configuration, &block)
|
37
39
|
end
|
38
40
|
|
41
|
+
def title(str, options = { nodoc: false })
|
42
|
+
configuration[:title] = options.merge(info: str)
|
43
|
+
end
|
44
|
+
|
45
|
+
def description(str, options = { nodoc: false })
|
46
|
+
configuration[:description] = options.merge(info: str)
|
47
|
+
end
|
48
|
+
|
49
|
+
def nodoc!
|
50
|
+
configuration[:nodoc] = true
|
51
|
+
end
|
52
|
+
|
39
53
|
# Declare a helper module or block whose methods will be available in dynamic fields and associations.
|
40
54
|
def helper(mod = nil, &block)
|
41
55
|
if mod
|
@@ -47,9 +61,14 @@ module Brainstem
|
|
47
61
|
end
|
48
62
|
end
|
49
63
|
|
64
|
+
|
50
65
|
# @overload default_sort_order(sort_string)
|
51
66
|
# Sets a default sort order.
|
52
|
-
# @param [String] sort_string The sort order to apply by default
|
67
|
+
# @param [String] sort_string The sort order to apply by default
|
68
|
+
# while presenting. The string must contain the name of a sort order
|
69
|
+
# that has explicitly been declared using {sort_order}. The string
|
70
|
+
# may end in +:asc+ or +:desc+ to indicate the default order's
|
71
|
+
# direction.
|
53
72
|
# @return [String] The new default sort order.
|
54
73
|
# @overload default_sort_order
|
55
74
|
# @return [String] The default sort order, or nil if one is not set.
|
@@ -58,31 +77,70 @@ module Brainstem
|
|
58
77
|
configuration[:default_sort_order]
|
59
78
|
end
|
60
79
|
|
61
|
-
|
80
|
+
|
81
|
+
#
|
82
|
+
# @overload sort_order(name, order, options)
|
62
83
|
# @param [Symbol] name The name of the sort order.
|
63
|
-
# @param [String] order The SQL string to use to sort the presented
|
64
|
-
#
|
65
|
-
# @
|
66
|
-
# @
|
67
|
-
#
|
84
|
+
# @param [String] order The SQL string to use to sort the presented
|
85
|
+
# data.
|
86
|
+
# @param [Hash] options
|
87
|
+
# @option options [String] :info Docstring for the sort order
|
88
|
+
# @option options [Boolean] :nodoc Whether this sort order be
|
89
|
+
# included in the generated documentation
|
90
|
+
#
|
91
|
+
# @overload sort_order(name, options, &block)
|
92
|
+
# @yieldparam scope [ActiveRecord::Relation] The scope representing
|
93
|
+
# the data being presented.
|
94
|
+
# @yieldreturn [ActiveRecord::Relation] A new scope that adds
|
95
|
+
# ordering requirements to the scope that was yielded.
|
96
|
+
#
|
97
|
+
# Create a named sort order, either containing a string to use as
|
98
|
+
# ORDER in a query, or with a block that adds an order Arel predicate
|
99
|
+
# to a scope.
|
100
|
+
#
|
68
101
|
# @raise [ArgumentError] if neither an order string or block is given.
|
69
|
-
|
102
|
+
#
|
103
|
+
def sort_order(name, *args, &block)
|
104
|
+
valid_options = %w(info nodoc)
|
105
|
+
options = args.extract_options!
|
106
|
+
.select { |k, v| valid_options.include?(k.to_s) }
|
107
|
+
order = args.first
|
108
|
+
|
70
109
|
raise ArgumentError, "A sort order must be given" unless block_given? || order
|
71
|
-
configuration[:sort_orders][name] = (
|
110
|
+
configuration[:sort_orders][name] = options.merge({
|
111
|
+
value: (block_given? ? block : order)
|
112
|
+
})
|
72
113
|
end
|
73
114
|
|
115
|
+
|
116
|
+
#
|
74
117
|
# @overload filter(name, options = {})
|
75
|
-
# @param [Symbol] name The name of the scope that may be applied as a
|
76
|
-
#
|
118
|
+
# @param [Symbol] name The name of the scope that may be applied as a
|
119
|
+
# filter.
|
120
|
+
# @option options [Object] :default If set, causes this filter to be
|
121
|
+
# applied to every request. If the filter accepts parameters, the
|
122
|
+
# value given here will be passed to the filter when it is applied.
|
123
|
+
# @option options [String] :info Docstring for the filter.
|
124
|
+
#
|
77
125
|
# @overload filter(name, options = {}, &block)
|
78
126
|
# @param [Symbol] name The filter can be requested using this name.
|
79
|
-
# @yieldparam scope [ActiveRecord::Relation] The scope that the
|
80
|
-
#
|
81
|
-
# @
|
127
|
+
# @yieldparam scope [ActiveRecord::Relation] The scope that the
|
128
|
+
# filter should use as a base.
|
129
|
+
# @yieldparam arg [Object] The argument passed when the filter was
|
130
|
+
# requested.
|
131
|
+
# @yieldreturn [ActiveRecord::Relation] A new scope that filters the
|
132
|
+
# scope that was yielded.
|
133
|
+
#
|
82
134
|
def filter(name, options = {}, &block)
|
83
|
-
|
135
|
+
valid_options = %w(default info include_params nodoc)
|
136
|
+
options.select! { |k, v| valid_options.include?(k.to_s) }
|
137
|
+
|
138
|
+
configuration[:filters][name] = options.merge({
|
139
|
+
value: (block_given? ? block : nil)
|
140
|
+
})
|
84
141
|
end
|
85
142
|
|
143
|
+
|
86
144
|
def search(&block)
|
87
145
|
configuration[:search] = block
|
88
146
|
end
|
@@ -104,6 +162,9 @@ module Brainstem
|
|
104
162
|
configuration.nest!(:filters)
|
105
163
|
configuration.nest!(:sort_orders)
|
106
164
|
configuration.nest!(:associations)
|
165
|
+
configuration.nonheritable!(:title)
|
166
|
+
configuration.nonheritable!(:description)
|
167
|
+
configuration.nonheritable!(:nodoc)
|
107
168
|
end
|
108
169
|
end
|
109
170
|
end
|
@@ -1,15 +1,18 @@
|
|
1
1
|
require 'brainstem/concerns/controller_param_management'
|
2
2
|
require 'brainstem/concerns/error_presentation'
|
3
|
+
require 'brainstem/concerns/controller_dsl'
|
3
4
|
|
4
5
|
module Brainstem
|
5
6
|
|
6
|
-
# ControllerMethods are intended to be included into controllers that will be
|
7
|
-
# The present method will pass
|
8
|
-
#
|
7
|
+
# ControllerMethods are intended to be included into controllers that will be
|
8
|
+
# handling requests for presented objects. The present method will pass
|
9
|
+
# through +params+, so that any allowed and requested includes, filters, sort
|
10
|
+
# orders will be applied to the presented data.
|
9
11
|
module ControllerMethods
|
10
12
|
extend ActiveSupport::Concern
|
11
13
|
include Concerns::ControllerParamManagement
|
12
14
|
include Concerns::ErrorPresentation
|
15
|
+
include Concerns::ControllerDSL
|
13
16
|
|
14
17
|
# Return a Ruby hash that contains models requested by the user's params and allowed
|
15
18
|
# by the +name+ presenter's configuration.
|
@@ -5,15 +5,18 @@ module Brainstem
|
|
5
5
|
class Association
|
6
6
|
include Brainstem::Concerns::Lookup
|
7
7
|
|
8
|
-
attr_reader :name, :target_class, :
|
8
|
+
attr_reader :name, :target_class, :options
|
9
9
|
|
10
|
-
def initialize(name, target_class,
|
10
|
+
def initialize(name, target_class, options)
|
11
11
|
@name = name.to_s
|
12
12
|
@target_class = target_class
|
13
|
-
@description = description
|
14
13
|
@options = options
|
15
14
|
end
|
16
15
|
|
16
|
+
def description
|
17
|
+
options[:info].presence
|
18
|
+
end
|
19
|
+
|
17
20
|
def method_name
|
18
21
|
if options[:dynamic] || options[:lookup]
|
19
22
|
nil
|
@@ -2,9 +2,12 @@ module Brainstem
|
|
2
2
|
module Concerns
|
3
3
|
module PresenterDSL
|
4
4
|
class AssociationsBlock < BaseBlock
|
5
|
-
def association(name, target_class,
|
6
|
-
|
7
|
-
|
5
|
+
def association(name, target_class, options = {})
|
6
|
+
configuration[:associations][name] = DSL::Association.new(
|
7
|
+
name,
|
8
|
+
target_class,
|
9
|
+
block_options.merge(format_options(options))
|
10
|
+
)
|
8
11
|
end
|
9
12
|
end
|
10
13
|
end
|
@@ -20,10 +20,8 @@ module Brainstem
|
|
20
20
|
klass.new(new_config, block_options.merge(new_options), &block)
|
21
21
|
end
|
22
22
|
|
23
|
-
def
|
24
|
-
options
|
25
|
-
description = args.shift
|
26
|
-
[description, options]
|
23
|
+
def format_options(options)
|
24
|
+
options.symbolize_keys
|
27
25
|
end
|
28
26
|
end
|
29
27
|
end
|
@@ -1,13 +1,17 @@
|
|
1
1
|
module Brainstem
|
2
2
|
module DSL
|
3
3
|
class Conditional
|
4
|
-
attr_reader :name, :type, :action, :
|
4
|
+
attr_reader :name, :type, :action, :options
|
5
5
|
|
6
|
-
def initialize(name, type, action,
|
6
|
+
def initialize(name, type, action, options = {})
|
7
7
|
@name = name
|
8
8
|
@type = type
|
9
9
|
@action = action
|
10
|
-
@
|
10
|
+
@options = options
|
11
|
+
end
|
12
|
+
|
13
|
+
def description
|
14
|
+
options[:info].presence
|
11
15
|
end
|
12
16
|
|
13
17
|
def matches?(model, helper_instance = Object.new, conditional_cache = { model: {}, request: {} })
|
@@ -2,12 +2,12 @@ module Brainstem
|
|
2
2
|
module Concerns
|
3
3
|
module PresenterDSL
|
4
4
|
class ConditionalsBlock < BaseBlock
|
5
|
-
def request(name, action,
|
6
|
-
configuration[:conditionals][name] = DSL::Conditional.new(name, :request, action,
|
5
|
+
def request(name, action, options = {})
|
6
|
+
configuration[:conditionals][name] = DSL::Conditional.new(name, :request, action, format_options(options))
|
7
7
|
end
|
8
8
|
|
9
|
-
def model(name, action,
|
10
|
-
configuration[:conditionals][name] = DSL::Conditional.new(name, :model, action,
|
9
|
+
def model(name, action, options = {})
|
10
|
+
configuration[:conditionals][name] = DSL::Conditional.new(name, :model, action, format_options(options))
|
11
11
|
end
|
12
12
|
end
|
13
13
|
end
|
@@ -1,18 +1,38 @@
|
|
1
1
|
require 'active_support/hash_with_indifferent_access'
|
2
|
+
require 'forwardable'
|
2
3
|
|
3
4
|
# A hash-like object that accepts a parent configuration object that defers to
|
4
5
|
# the parent in the absence of one of its own keys (thus simulating inheritance).
|
5
6
|
module Brainstem
|
6
7
|
module DSL
|
7
8
|
class Configuration
|
9
|
+
extend Forwardable
|
8
10
|
|
9
11
|
# Returns a new configuration object.
|
10
12
|
#
|
11
13
|
# @params [Object] parent_configuration The parent configuration object
|
12
14
|
# which the new configuration object should use as a base.
|
13
15
|
def initialize(parent_configuration = nil)
|
14
|
-
@parent_configuration
|
15
|
-
@storage
|
16
|
+
@parent_configuration = parent_configuration || ActiveSupport::HashWithIndifferentAccess.new
|
17
|
+
@storage = ActiveSupport::HashWithIndifferentAccess.new
|
18
|
+
|
19
|
+
#
|
20
|
+
# Nonheritable keys are a bit peculiar: they make the lookup for a key
|
21
|
+
# specified as nonheritable to return no result when it falls back to
|
22
|
+
# the parent configuration.
|
23
|
+
#
|
24
|
+
# These keys themselves are inheritable; in this way, a class that
|
25
|
+
# descends from another will keep the same behaviour as its superclass
|
26
|
+
# without necessarily having the same data. Or to put it another way,
|
27
|
+
# if you have specified that 'title' is not inheritable in a
|
28
|
+
# superclass's configuration, that is a property of that class, and
|
29
|
+
# descendent classes should behave the same way.
|
30
|
+
#
|
31
|
+
# It is also unlikely that subclasses will modify the list of
|
32
|
+
# nonheritable keys.
|
33
|
+
parent_nh_keys = parent_configuration &&
|
34
|
+
parent_configuration.nonheritable_keys
|
35
|
+
@nonheritable_keys = InheritableAppendSet.new(parent_nh_keys)
|
16
36
|
end
|
17
37
|
|
18
38
|
def [](key)
|
@@ -40,12 +60,161 @@ module Brainstem
|
|
40
60
|
@storage[key] ||= InheritableAppendSet.new
|
41
61
|
end
|
42
62
|
|
63
|
+
|
64
|
+
#
|
65
|
+
# Marks a key in the configuration as nonheritable, which means that the key:
|
66
|
+
#
|
67
|
+
# - will appear in the list of keys for this object;
|
68
|
+
# - will return its value when fetched from this object;
|
69
|
+
# - will be included in the +to_h+ output from this object;
|
70
|
+
# - will be included when iterating with +#each+ from this object;
|
71
|
+
#
|
72
|
+
# - will not appear in the list of keys for any child object;
|
73
|
+
# - will return +nil+ when fetched from any child object;
|
74
|
+
# - will not be included in the +#to_h+ output from any child object;
|
75
|
+
# - will not be included when iterating with +#each+ from any child object.
|
76
|
+
#
|
77
|
+
# @param [Symbol,String] key the key to append to the list of nonheritable
|
78
|
+
# keys
|
79
|
+
#
|
80
|
+
def nonheritable!(key)
|
81
|
+
key = key.to_s
|
82
|
+
self.nonheritable_keys << key unless self.nonheritable_keys.include?(key)
|
83
|
+
end
|
84
|
+
|
85
|
+
|
86
|
+
attr_accessor :nonheritable_keys
|
87
|
+
|
88
|
+
|
89
|
+
#
|
90
|
+
# Returns the keys in this configuration object that are visible to child
|
91
|
+
# configuration objects (i.e. heritable keys).
|
92
|
+
#
|
93
|
+
# @return [Array] keys
|
94
|
+
#
|
95
|
+
def keys_visible_to_children
|
96
|
+
keys - nonheritable_keys.to_a
|
97
|
+
end
|
98
|
+
|
99
|
+
|
100
|
+
#
|
101
|
+
# Returns a hash of this object's storage, less those pairs that are
|
102
|
+
# not visible to children.
|
103
|
+
#
|
104
|
+
# @return [Hash] the hash, less nonheritable pairs.
|
105
|
+
#
|
106
|
+
def pairs_visible_to_children
|
107
|
+
to_h.select {|k, v| keys_visible_to_children.include?(k.to_s) }
|
108
|
+
end
|
109
|
+
|
110
|
+
|
111
|
+
#
|
112
|
+
# Returns the union of all keys in this configuration plus those that are
|
113
|
+
# heritable in the parent.
|
114
|
+
#
|
115
|
+
# @return [Array] keys
|
116
|
+
#
|
43
117
|
def keys
|
44
|
-
@parent_configuration.
|
118
|
+
if @parent_configuration.respond_to?(:keys_visible_to_children)
|
119
|
+
@parent_configuration.keys_visible_to_children | @storage.keys
|
120
|
+
else
|
121
|
+
@parent_configuration.keys | @storage.keys
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
|
126
|
+
#
|
127
|
+
# Returns a list of nonheritable keys for the parent configuration, if
|
128
|
+
# the parent configuration actually keeps track of it. Otherwise returns
|
129
|
+
# an empty array.
|
130
|
+
#
|
131
|
+
# @return [Array<String>] the list of nonheritable keys in the
|
132
|
+
# parent configuration.
|
133
|
+
#
|
134
|
+
def parent_nonheritable_keys
|
135
|
+
if @parent_configuration.respond_to?(:nonheritable_keys)
|
136
|
+
@parent_configuration.nonheritable_keys
|
137
|
+
else
|
138
|
+
[]
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
|
143
|
+
#
|
144
|
+
# Returns whether a key is nonheritable in this configuration object's
|
145
|
+
# parent configuration.
|
146
|
+
#
|
147
|
+
# Is of arity -1 so it can be easily passed to methods that yield
|
148
|
+
# either a key, or a key/value tuple.
|
149
|
+
#
|
150
|
+
# @param [Symbol,String] key the key to check for nonheritability.
|
151
|
+
#
|
152
|
+
def key_nonheritable_in_parent?(*key)
|
153
|
+
parent_nonheritable_keys.include?(key.first.to_s)
|
154
|
+
end
|
155
|
+
|
156
|
+
|
157
|
+
#
|
158
|
+
# An inversion of +key_nonheritable_in_parent+. Returns true if the
|
159
|
+
# key is not marked as nonheritable in the parent configuration.
|
160
|
+
#
|
161
|
+
# Is of arity -1 so it can be easily passed to methods that yield
|
162
|
+
# either a key, or a key/value tuple.
|
163
|
+
#
|
164
|
+
# @param [Symbol,String] key the key to check for heritability.
|
165
|
+
#
|
166
|
+
def key_inheritable_in_parent?(*key)
|
167
|
+
!key_nonheritable_in_parent?(key.first.to_s)
|
168
|
+
end
|
169
|
+
|
170
|
+
|
171
|
+
#
|
172
|
+
# Returns a hash of this object's storage merged over the heritable pairs
|
173
|
+
# of its parent configurations.
|
174
|
+
#
|
175
|
+
# @return [Hash] the merged hash
|
176
|
+
#
|
177
|
+
def to_h
|
178
|
+
if @parent_configuration.respond_to?(:pairs_visible_to_children)
|
179
|
+
@parent_configuration.pairs_visible_to_children.merge(@storage)
|
180
|
+
else
|
181
|
+
@parent_configuration.to_h.merge(@storage)
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
|
186
|
+
#
|
187
|
+
# Returns the value for the given key, or if it could not be found:
|
188
|
+
# - Raises a +KeyError+ if not passed a default or a block;
|
189
|
+
# - Returns the default if it is passed a default but no block;
|
190
|
+
# - Calls and returns the block if passed a block but no default;
|
191
|
+
# - Calls the block with the default and returns the block if passed a
|
192
|
+
# default and a block.
|
193
|
+
#
|
194
|
+
# @params [Symbol,String] key the key to look up
|
195
|
+
# @params [Object] default the default to return
|
196
|
+
# @params [Proc] block the block to call
|
197
|
+
#
|
198
|
+
# @see http://ruby-doc.org/core-2.2.1/Hash.html#method-i-fetch
|
199
|
+
#
|
200
|
+
def fetch(key, default = nil, &block)
|
201
|
+
val = get!(key)
|
202
|
+
return val if val
|
203
|
+
|
204
|
+
if default && !block_given?
|
205
|
+
default
|
206
|
+
elsif block_given?
|
207
|
+
default ? block.call(default) : block.call
|
208
|
+
else
|
209
|
+
raise KeyError
|
210
|
+
end
|
45
211
|
end
|
46
212
|
|
213
|
+
|
47
214
|
def has_key?(key)
|
48
|
-
@storage.has_key?(key) ||
|
215
|
+
@storage.has_key?(key) ||
|
216
|
+
(@parent_configuration.has_key?(key) &&
|
217
|
+
key_inheritable_in_parent?(key))
|
49
218
|
end
|
50
219
|
|
51
220
|
def length
|
@@ -58,7 +227,7 @@ module Brainstem
|
|
58
227
|
end
|
59
228
|
end
|
60
229
|
|
61
|
-
delegate :empty
|
230
|
+
delegate :empty? => :keys
|
62
231
|
|
63
232
|
private
|
64
233
|
|
@@ -67,14 +236,19 @@ module Brainstem
|
|
67
236
|
# Retrieves the value stored at key.
|
68
237
|
#
|
69
238
|
# - If +key+ is already defined, it returns that;
|
239
|
+
# - If +key+ in the parent is marked as nonheritable, it returns
|
240
|
+
# +nil+;
|
70
241
|
# - If +key+ in the parent is a +Configuration+, returns a new
|
71
242
|
# +Configuration+ with the parent set;
|
72
243
|
# - If +key+ in the parent is an +InheritableAppendSet+, returns a new
|
73
244
|
# +InheritableAppendSet+ with the parent set;
|
74
245
|
# - Elsewise returns the parent configuration's value for the key.
|
246
|
+
#
|
75
247
|
def get!(key)
|
76
248
|
@storage[key] || begin
|
77
|
-
if
|
249
|
+
if key_nonheritable_in_parent?(key)
|
250
|
+
nil
|
251
|
+
elsif @parent_configuration[key].is_a?(Configuration)
|
78
252
|
@storage[key] = Configuration.new(@parent_configuration[key])
|
79
253
|
elsif @parent_configuration[key].is_a?(InheritableAppendSet)
|
80
254
|
@storage[key] = InheritableAppendSet.new(@parent_configuration[key])
|
@@ -87,6 +261,8 @@ module Brainstem
|
|
87
261
|
# An Array-like object that provides `push`, `concat`, `each`, `empty?`, and `to_a` methods that act the combination
|
88
262
|
# of its own entries and those of a parent InheritableAppendSet, if present.
|
89
263
|
class InheritableAppendSet
|
264
|
+
extend Forwardable
|
265
|
+
|
90
266
|
def initialize(parent_array = nil)
|
91
267
|
@parent_array = parent_array || []
|
92
268
|
@storage = []
|
@@ -105,8 +281,8 @@ module Brainstem
|
|
105
281
|
@parent_array.to_a + @storage
|
106
282
|
end
|
107
283
|
|
108
|
-
delegate :each, :empty?,
|
284
|
+
delegate [:each, :empty?, :include?] => :to_a
|
109
285
|
end
|
110
286
|
end
|
111
287
|
end
|
112
|
-
end
|
288
|
+
end
|