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.
Files changed (103) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +12 -0
  3. data/Gemfile.lock +1 -1
  4. data/README.md +383 -32
  5. data/bin/brainstem +6 -0
  6. data/brainstem.gemspec +2 -0
  7. data/docs/api_doc_generator.markdown +175 -0
  8. data/docs/brainstem_executable.markdown +32 -0
  9. data/docs/docgen.png +0 -0
  10. data/docs/docgen_ascii.txt +63 -0
  11. data/docs/executable.png +0 -0
  12. data/docs/executable_ascii.txt +10 -0
  13. data/lib/brainstem/api_docs.rb +146 -0
  14. data/lib/brainstem/api_docs/abstract_collection.rb +116 -0
  15. data/lib/brainstem/api_docs/atlas.rb +158 -0
  16. data/lib/brainstem/api_docs/builder.rb +167 -0
  17. data/lib/brainstem/api_docs/controller.rb +122 -0
  18. data/lib/brainstem/api_docs/controller_collection.rb +40 -0
  19. data/lib/brainstem/api_docs/endpoint.rb +234 -0
  20. data/lib/brainstem/api_docs/endpoint_collection.rb +58 -0
  21. data/lib/brainstem/api_docs/exceptions.rb +8 -0
  22. data/lib/brainstem/api_docs/formatters/abstract_formatter.rb +64 -0
  23. data/lib/brainstem/api_docs/formatters/markdown/controller_formatter.rb +76 -0
  24. data/lib/brainstem/api_docs/formatters/markdown/endpoint_collection_formatter.rb +73 -0
  25. data/lib/brainstem/api_docs/formatters/markdown/endpoint_formatter.rb +169 -0
  26. data/lib/brainstem/api_docs/formatters/markdown/helper.rb +76 -0
  27. data/lib/brainstem/api_docs/formatters/markdown/presenter_formatter.rb +200 -0
  28. data/lib/brainstem/api_docs/introspectors/abstract_introspector.rb +100 -0
  29. data/lib/brainstem/api_docs/introspectors/rails_introspector.rb +232 -0
  30. data/lib/brainstem/api_docs/presenter.rb +225 -0
  31. data/lib/brainstem/api_docs/presenter_collection.rb +97 -0
  32. data/lib/brainstem/api_docs/resolver.rb +73 -0
  33. data/lib/brainstem/api_docs/sinks/abstract_sink.rb +37 -0
  34. data/lib/brainstem/api_docs/sinks/controller_presenter_multifile_sink.rb +93 -0
  35. data/lib/brainstem/api_docs/sinks/stdout_sink.rb +44 -0
  36. data/lib/brainstem/cli.rb +146 -0
  37. data/lib/brainstem/cli/abstract_command.rb +97 -0
  38. data/lib/brainstem/cli/generate_api_docs_command.rb +169 -0
  39. data/lib/brainstem/concerns/controller_dsl.rb +300 -0
  40. data/lib/brainstem/concerns/controller_param_management.rb +30 -9
  41. data/lib/brainstem/concerns/formattable.rb +38 -0
  42. data/lib/brainstem/concerns/inheritable_configuration.rb +3 -2
  43. data/lib/brainstem/concerns/optional.rb +43 -0
  44. data/lib/brainstem/concerns/presenter_dsl.rb +76 -15
  45. data/lib/brainstem/controller_methods.rb +6 -3
  46. data/lib/brainstem/dsl/association.rb +6 -3
  47. data/lib/brainstem/dsl/associations_block.rb +6 -3
  48. data/lib/brainstem/dsl/base_block.rb +2 -4
  49. data/lib/brainstem/dsl/conditional.rb +7 -3
  50. data/lib/brainstem/dsl/conditionals_block.rb +4 -4
  51. data/lib/brainstem/dsl/configuration.rb +184 -8
  52. data/lib/brainstem/dsl/field.rb +6 -3
  53. data/lib/brainstem/dsl/fields_block.rb +2 -3
  54. data/lib/brainstem/help_text.txt +8 -0
  55. data/lib/brainstem/presenter.rb +27 -6
  56. data/lib/brainstem/presenter_validator.rb +5 -2
  57. data/lib/brainstem/time_classes.rb +1 -1
  58. data/lib/brainstem/version.rb +1 -1
  59. data/spec/brainstem/api_docs/abstract_collection_spec.rb +156 -0
  60. data/spec/brainstem/api_docs/atlas_spec.rb +353 -0
  61. data/spec/brainstem/api_docs/builder_spec.rb +100 -0
  62. data/spec/brainstem/api_docs/controller_collection_spec.rb +92 -0
  63. data/spec/brainstem/api_docs/controller_spec.rb +225 -0
  64. data/spec/brainstem/api_docs/endpoint_collection_spec.rb +144 -0
  65. data/spec/brainstem/api_docs/endpoint_spec.rb +346 -0
  66. data/spec/brainstem/api_docs/formatters/abstract_formatter_spec.rb +30 -0
  67. data/spec/brainstem/api_docs/formatters/markdown/controller_formatter_spec.rb +126 -0
  68. data/spec/brainstem/api_docs/formatters/markdown/endpoint_collection_formatter_spec.rb +85 -0
  69. data/spec/brainstem/api_docs/formatters/markdown/endpoint_formatter_spec.rb +261 -0
  70. data/spec/brainstem/api_docs/formatters/markdown/helper_spec.rb +100 -0
  71. data/spec/brainstem/api_docs/formatters/markdown/presenter_formatter_spec.rb +485 -0
  72. data/spec/brainstem/api_docs/introspectors/abstract_introspector_spec.rb +192 -0
  73. data/spec/brainstem/api_docs/introspectors/rails_introspector_spec.rb +170 -0
  74. data/spec/brainstem/api_docs/presenter_collection_spec.rb +84 -0
  75. data/spec/brainstem/api_docs/presenter_spec.rb +519 -0
  76. data/spec/brainstem/api_docs/resolver_spec.rb +72 -0
  77. data/spec/brainstem/api_docs/sinks/abstract_sink_spec.rb +16 -0
  78. data/spec/brainstem/api_docs/sinks/controller_presenter_multifile_sink_spec.rb +56 -0
  79. data/spec/brainstem/api_docs/sinks/stdout_sink_spec.rb +22 -0
  80. data/spec/brainstem/api_docs_spec.rb +58 -0
  81. data/spec/brainstem/cli/abstract_command_spec.rb +91 -0
  82. data/spec/brainstem/cli/generate_api_docs_command_spec.rb +125 -0
  83. data/spec/brainstem/cli_spec.rb +67 -0
  84. data/spec/brainstem/concerns/controller_dsl_spec.rb +471 -0
  85. data/spec/brainstem/concerns/controller_param_management_spec.rb +36 -16
  86. data/spec/brainstem/concerns/formattable_spec.rb +30 -0
  87. data/spec/brainstem/concerns/inheritable_configuration_spec.rb +104 -4
  88. data/spec/brainstem/concerns/optional_spec.rb +48 -0
  89. data/spec/brainstem/concerns/presenter_dsl_spec.rb +202 -31
  90. data/spec/brainstem/dsl/association_spec.rb +18 -2
  91. data/spec/brainstem/dsl/conditional_spec.rb +25 -2
  92. data/spec/brainstem/dsl/configuration_spec.rb +1 -1
  93. data/spec/brainstem/dsl/field_spec.rb +18 -2
  94. data/spec/brainstem/presenter_collection_spec.rb +10 -2
  95. data/spec/brainstem/presenter_spec.rb +32 -0
  96. data/spec/brainstem/presenter_validator_spec.rb +12 -7
  97. data/spec/dummy/rails.rb +49 -0
  98. data/spec/shared/atlas_taker.rb +18 -0
  99. data/spec/shared/formattable.rb +14 -0
  100. data/spec/spec_helper.rb +2 -0
  101. data/spec/spec_helpers/db.rb +1 -1
  102. data/spec/spec_helpers/presenters.rb +20 -14
  103. metadata +106 -6
@@ -1,22 +1,43 @@
1
- # Provide `brainstem_model_name` and `brainstem_plural_model_name` in controllers for use when accessing the `params` hash.
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.presence || controller_name.singularize
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.presence || brainstem_model_name.pluralize
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 while presenting. The string must contain the name of a sort order that has explicitly been declared using {sort_order}. The string may end in +:asc+ or +:desc+ to indicate the default order's direction.
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
- # @overload sort_order(name, order)
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 data.
64
- # @overload sort_order(name, &block)
65
- # @yieldparam scope [ActiveRecord::Relation] The scope representing the data being presented.
66
- # @yieldreturn [ActiveRecord::Relation] A new scope that adds ordering requirements to the scope that was yielded.
67
- # Create a named sort order, either containing a string to use as ORDER in a query, or with a block that adds an order Arel predicate to a scope.
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
- def sort_order(name, order = nil, &block)
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] = (block_given? ? block : order)
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 filter.
76
- # @option options [Object] :default If set, causes this filter to be applied to every request. If the filter accepts parameters, the value given here will be passed to the filter when it is applied.
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 filter should use as a base.
80
- # @yieldparam arg [Object] The argument passed when the filter was requested.
81
- # @yieldreturn [ActiveRecord::Relation] A new scope that filters the scope that was yielded.
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
- configuration[:filters][name] = [options, (block_given? ? block : nil)]
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 handling requests for presented objects.
7
- # The present method will pass through +params+, so that any allowed and requested includes, filters, sort orders
8
- # will be applied to the presented data.
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, :description, :options
8
+ attr_reader :name, :target_class, :options
9
9
 
10
- def initialize(name, target_class, description, options)
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, *args)
6
- description, options = parse_args(args)
7
- configuration[:associations][name] = DSL::Association.new(name, target_class, description, block_options.merge(options))
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 parse_args(args)
24
- options = args.last.is_a?(Hash) ? args.pop : {}
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, :description
4
+ attr_reader :name, :type, :action, :options
5
5
 
6
- def initialize(name, type, action, description)
6
+ def initialize(name, type, action, options = {})
7
7
  @name = name
8
8
  @type = type
9
9
  @action = action
10
- @description = description
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, description = nil)
6
- configuration[:conditionals][name] = DSL::Conditional.new(name, :request, action, description)
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, description = nil)
10
- configuration[:conditionals][name] = DSL::Conditional.new(name, :model, action, description)
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 = parent_configuration || ActiveSupport::HashWithIndifferentAccess.new
15
- @storage = ActiveSupport::HashWithIndifferentAccess.new
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.keys | @storage.keys
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) || @parent_configuration.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?, to: :keys
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 @parent_configuration[key].is_a?(Configuration)
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?, to: :to_a
284
+ delegate [:each, :empty?, :include?] => :to_a
109
285
  end
110
286
  end
111
287
  end
112
- end
288
+ end