cloud-templates 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. checksums.yaml +7 -0
  2. data/.rubocop.yml +29 -0
  3. data/.simplecov +6 -0
  4. data/Gemfile +2 -0
  5. data/LICENSE +201 -0
  6. data/NOTICE +13 -0
  7. data/README.md +124 -0
  8. data/Rakefile +27 -0
  9. data/cloud-templates.gemspec +27 -0
  10. data/examples/lib/user_directory/artifacts/catalogized.rb +11 -0
  11. data/examples/lib/user_directory/artifacts/group.rb +37 -0
  12. data/examples/lib/user_directory/artifacts/ided.rb +11 -0
  13. data/examples/lib/user_directory/artifacts/organization.rb +17 -0
  14. data/examples/lib/user_directory/artifacts/pathed.rb +22 -0
  15. data/examples/lib/user_directory/artifacts/person.rb +20 -0
  16. data/examples/lib/user_directory/artifacts/team.rb +31 -0
  17. data/examples/lib/user_directory/artifacts/unit.rb +24 -0
  18. data/examples/lib/user_directory/artifacts/user.rb +29 -0
  19. data/examples/lib/user_directory/render/etc/artifact_view.rb +15 -0
  20. data/examples/lib/user_directory/render/etc/composite_view.rb +26 -0
  21. data/examples/lib/user_directory/render/etc/group_view.rb +23 -0
  22. data/examples/lib/user_directory/render/etc/person_view.rb +19 -0
  23. data/examples/lib/user_directory/render/etc/registry.rb +33 -0
  24. data/examples/lib/user_directory/render/etc/user_view.rb +35 -0
  25. data/examples/lib/user_directory/render/etc.rb +3 -0
  26. data/examples/lib/user_directory/render/ldap/artifact_view.rb +27 -0
  27. data/examples/lib/user_directory/render/ldap/composite_view.rb +32 -0
  28. data/examples/lib/user_directory/render/ldap/group_view.rb +28 -0
  29. data/examples/lib/user_directory/render/ldap/organization_view.rb +26 -0
  30. data/examples/lib/user_directory/render/ldap/person_view.rb +39 -0
  31. data/examples/lib/user_directory/render/ldap/registry.rb +16 -0
  32. data/examples/lib/user_directory/render/ldap/unit_view.rb +26 -0
  33. data/examples/lib/user_directory/render/ldap/user_view.rb +39 -0
  34. data/examples/lib/user_directory/render/ldap.rb +3 -0
  35. data/examples/lib/user_directory/utils.rb +18 -0
  36. data/examples/lib/user_directory.rb +23 -0
  37. data/examples/lib_path.rb +2 -0
  38. data/examples/spec/spec_helper.rb +1 -0
  39. data/examples/spec/user_directory_spec.rb +568 -0
  40. data/lib/aws/templates/artifact.rb +140 -0
  41. data/lib/aws/templates/composite.rb +178 -0
  42. data/lib/aws/templates/exceptions.rb +221 -0
  43. data/lib/aws/templates/render/registry.rb +60 -0
  44. data/lib/aws/templates/render/utils/base_type_views.rb +131 -0
  45. data/lib/aws/templates/render/view.rb +127 -0
  46. data/lib/aws/templates/render.rb +72 -0
  47. data/lib/aws/templates/utils/artifact_storage.rb +141 -0
  48. data/lib/aws/templates/utils/contextualized/filters.rb +437 -0
  49. data/lib/aws/templates/utils/contextualized/hash.rb +13 -0
  50. data/lib/aws/templates/utils/contextualized/nil.rb +13 -0
  51. data/lib/aws/templates/utils/contextualized/proc.rb +13 -0
  52. data/lib/aws/templates/utils/contextualized.rb +113 -0
  53. data/lib/aws/templates/utils/default.rb +185 -0
  54. data/lib/aws/templates/utils/dependency/enumerable.rb +13 -0
  55. data/lib/aws/templates/utils/dependency/object.rb +46 -0
  56. data/lib/aws/templates/utils/dependency.rb +121 -0
  57. data/lib/aws/templates/utils/dependent.rb +28 -0
  58. data/lib/aws/templates/utils/inheritable.rb +52 -0
  59. data/lib/aws/templates/utils/late_bound.rb +89 -0
  60. data/lib/aws/templates/utils/memoized.rb +27 -0
  61. data/lib/aws/templates/utils/named.rb +19 -0
  62. data/lib/aws/templates/utils/options.rb +279 -0
  63. data/lib/aws/templates/utils/parametrized/constraints.rb +423 -0
  64. data/lib/aws/templates/utils/parametrized/getters.rb +293 -0
  65. data/lib/aws/templates/utils/parametrized/guarded.rb +32 -0
  66. data/lib/aws/templates/utils/parametrized/mapper.rb +73 -0
  67. data/lib/aws/templates/utils/parametrized/nested.rb +72 -0
  68. data/lib/aws/templates/utils/parametrized/transformations.rb +660 -0
  69. data/lib/aws/templates/utils/parametrized.rb +240 -0
  70. data/lib/aws/templates/utils.rb +219 -0
  71. data/lib/aws/templates.rb +16 -0
  72. data/spec/aws/templates/artifact_spec.rb +161 -0
  73. data/spec/aws/templates/composite_spec.rb +361 -0
  74. data/spec/aws/templates/render/utils/base_type_views_spec.rb +104 -0
  75. data/spec/aws/templates/render_spec.rb +62 -0
  76. data/spec/aws/templates/utils/as_named_spec.rb +31 -0
  77. data/spec/aws/templates/utils/contextualized/filters_spec.rb +108 -0
  78. data/spec/aws/templates/utils/contextualized_spec.rb +115 -0
  79. data/spec/aws/templates/utils/late_bound_spec.rb +52 -0
  80. data/spec/aws/templates/utils/options_spec.rb +67 -0
  81. data/spec/aws/templates/utils/parametrized/constraint_spec.rb +175 -0
  82. data/spec/aws/templates/utils/parametrized/getters_spec.rb +139 -0
  83. data/spec/aws/templates/utils/parametrized/transformation_spec.rb +314 -0
  84. data/spec/aws/templates/utils/parametrized_spec.rb +241 -0
  85. data/spec/spec_helper.rb +6 -0
  86. metadata +244 -0
@@ -0,0 +1,140 @@
1
+ require 'aws/templates/utils/parametrized'
2
+ require 'aws/templates/utils/parametrized/getters'
3
+ require 'aws/templates/utils/parametrized/constraints'
4
+ require 'aws/templates/utils/default'
5
+ require 'aws/templates/utils/options'
6
+ require 'aws/templates/utils/dependent'
7
+
8
+ module Aws
9
+ module Templates
10
+ ##
11
+ # Basic artifact
12
+ #
13
+ # "Artifact" in the terminology of the framework is an entity which
14
+ # can represent any parametrizable object type from any problem domain
15
+ # possible. For instance, for CloudFormation, artifacts are resources
16
+ # in a CFN template, the template itself
17
+ #
18
+ # If your target domain is infrastructure orchestration, artifact is
19
+ # usually a single entity such as S3 volume, ASG, DynamoDB table, etc.
20
+ # However, the notion of artifact is not particularly fixed since the
21
+ # framework can be used for different purposes including documents
22
+ # generation and general templating tasks. Basically, artifact is a
23
+ # single parametrizable entity or a step in input hash
24
+ # transformations.
25
+ #
26
+ # Artifact classes work together with meta-programming mechanisms in
27
+ # Ruby language enabling artifacts inheritance in the natural way
28
+ # using simple Ruby classes. The feature is useful when you have a
29
+ # group of artifacts which share the same basic parameters but differ
30
+ # in details.
31
+ #
32
+ # The central part in the framework is played by processed hash. All
33
+ # mechanisms are based on simple ad-hoc merging rules which are
34
+ # described at merge method but basis can be described as following:
35
+ # each superclass initializer accepts children class processed hash
36
+ # as input hash and the hash is processed recursively through
37
+ # class hierarchy. Old values are newer removed by default so the
38
+ # whole process is no more than continuous hash expansion.
39
+ #
40
+ # class Piece < Artifact
41
+ # default { { output: options[:param] } }
42
+ # end
43
+ #
44
+ # class ConcretePiece < Piece
45
+ # default param: 'Came from child'
46
+ # end
47
+ #
48
+ # Piece.new(a: 1, b: 2).options.to_hash # => { a: 1, b: 2, output: nil }
49
+ # ConcretePiece.new(a: 1, b: 2).options.to_hash
50
+ # # => { a: 1, b: 2, output: 'Came from child', param: 'Came from child' }
51
+ #
52
+ # Also, as one of the peculiarities of the framework, you can override
53
+ # any auto-generated parameter with input hash if they have the same
54
+ # name/path.
55
+ class Artifact
56
+ include Utils::Default
57
+ include Utils::Parametrized
58
+ include Utils::Dependent
59
+
60
+ attr_accessor :options
61
+
62
+ def self.getter
63
+ as_is
64
+ end
65
+
66
+ def self.to_s
67
+ return super unless name.nil?
68
+ "<Subclass of (#{superclass}) with features #{features}>"
69
+ end
70
+
71
+ def self.features
72
+ @features ||= ancestors.take_while { |mod| mod != superclass }
73
+ end
74
+
75
+ # Create new child class with mixins
76
+ #
77
+ # The class method is useful when you want to mix-in some behavior adjustments
78
+ # without creating a new named class. For instance when you want to mix-in
79
+ # some defaults into class and instantiate a few instances out of that.
80
+ def self.featuring(*modules)
81
+ return self if modules.empty?
82
+
83
+ modules.inject(Class.new(self)) do |klass, mod|
84
+ klass.send(:include, mod)
85
+ end
86
+ end
87
+
88
+ ##
89
+ # Artifact's label
90
+ #
91
+ # All artifacts have labels assigned to them to simplify reverse
92
+ # look-up while linking dependencies. Interpretation of this field is purely
93
+ # application-specific.
94
+ default label: proc { object_id }
95
+
96
+ parameter :label, description: 'Artifact\'s label', constraint: not_nil
97
+
98
+ ##
99
+ # Artifact's root
100
+ #
101
+ # A root is an object which bundles artifacts into common rendering group helping to find
102
+ # disconnected pieces of dependency graph. If two artifacts have different roots they
103
+ # definitelly belong to different graphs.
104
+ default root: proc { object_id }
105
+
106
+ def root
107
+ options[:root]
108
+ end
109
+
110
+ ##
111
+ # Artifact's parent
112
+ #
113
+ # Artifacts can be organized into a hierarchy of composition. This field points back to the
114
+ # artifact's parent.
115
+ parameter :parent, description: 'Artifact parent'
116
+
117
+ ##
118
+ # Create a new artifact
119
+ #
120
+ # Artifact constructor. If you want to override it you might probably
121
+ # want to look into default first. All default values specified in the
122
+ # class definition and all its ancestors are processed and merged with
123
+ # options so it contains fully-processed hash.
124
+ #
125
+ # The algorithm of the processing is the following:
126
+ # * merge defaults hash with passed options; options take precedence
127
+ # * merge the hash with default calculations return results; calculations output
128
+ # takes preference
129
+ # * pass resulting hash to superclass initializer
130
+ # * merge resulting hash with options; options take preference
131
+ #
132
+ # * +params+ - input parameters hash to be used during following
133
+ # hash transformations and expansions.
134
+ def initialize(params)
135
+ @options = Utils::Options.new(params)
136
+ process_options(params)
137
+ end
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,178 @@
1
+ require 'aws/templates/artifact'
2
+ require 'aws/templates/utils/artifact_storage'
3
+ require 'aws/templates/utils/contextualized'
4
+ require 'aws/templates/utils/dependency'
5
+
6
+ module Aws
7
+ module Templates
8
+ ##
9
+ # Composite
10
+ #
11
+ # Composite is an artifact which contain other artifacts and provide
12
+ # DSL syntax sugar to define it
13
+ #
14
+ # A composite can represent complex entities as CloudFormation
15
+ # stacks which can contain settings and infrastructure artifacts inside it.
16
+ # However, it's still a single entity which may be versioned as a
17
+ # whole and can represent the current state of deployed application.
18
+ #
19
+ # Composite is still an artifact and has all methods inherited so
20
+ # besides grouping different artifacts alltogether you can process
21
+ # input parameters too.
22
+ #
23
+ # Composite is a recursive structure as a result of being an artifact
24
+ # so you can construct arbitrary deep hierarchies of objects. Also it
25
+ # supports inheritance as artifact does. So every component defined in
26
+ # the parent class will be initialized properly in all children too.
27
+ class Composite < Artifact
28
+ include Aws::Templates::Utils::Contextualized
29
+
30
+ # propagate root to the components and set itself as the parent
31
+ contextualize filter(:add, :root) & (filter(:override) { { parent: self } })
32
+
33
+ ##
34
+ # Dictionary of artifacts and their labels the composite is
35
+ # consisting of
36
+ #
37
+ # Accessor returning dictionary of artifacts currently residing in
38
+ # composite instance with labels as keys
39
+ def artifacts
40
+ @artifacts ||= Utils::ArtifactStorage.new
41
+ end
42
+
43
+ ##
44
+ # Shortcut for accessing artifacts by their labels
45
+ #
46
+ # The method returns either stored artifact object or throws a
47
+ # descriptive exception.
48
+ def [](artifact_label)
49
+ unless artifacts.key?(artifact_label)
50
+ raise "There is no artifact #{artifact_label}" \
51
+ " in composite #{label}"
52
+ end
53
+
54
+ artifacts[artifact_label].as_a_dependency.to_self
55
+ end
56
+
57
+ def []=(artifact_label, artifact_object)
58
+ if artifacts.key?(artifact_label)
59
+ if artifacts[artifact_label] != artifact_object.object
60
+ raise "Artifact #{artifact_label} is already present " \
61
+ "in composite #{label}"
62
+ end
63
+ else
64
+ artifacts[artifact_label] = artifact_object.object
65
+ end
66
+
67
+ artifact_object.as_a_dependency.to_self
68
+ end
69
+
70
+ ##
71
+ # Artifacts definition block for DSL
72
+ #
73
+ # An element of the framework DSL. Allows you to define
74
+ # composite's artifacts declaratively with using standard language
75
+ # features.
76
+ def self.components(*args, &blk)
77
+ define_method(:create_components) do
78
+ super()
79
+ instance_exec(*args, &blk)
80
+ end
81
+
82
+ self
83
+ end
84
+
85
+ ##
86
+ # Add components into the composite's instance
87
+ #
88
+ # Analog of class-level "components" method to add components after
89
+ # artifact creation when using class-level definitions are not
90
+ # appropriate
91
+ def components(&blk)
92
+ instance_exec(&blk)
93
+ self
94
+ end
95
+
96
+ ##
97
+ # Syntax sugar to create composite classes on spot
98
+ #
99
+ # Create a new child class of the current class and executes a
100
+ # block in the context of the class object optionally passing a list
101
+ # of arguments to it.
102
+ def self.for(*args, &blk)
103
+ klass = Class.new(self)
104
+ klass.instance_eval(*args, &blk) unless blk.nil?
105
+ klass
106
+ end
107
+
108
+ ##
109
+ # Artifact definition constructor in DSL
110
+ #
111
+ # Defines a single artifact in composite's definition block. This
112
+ # method was designed to be used inside of composite block but you
113
+ # can use it elsewhere else applied on a class instance.
114
+ #
115
+ # * +type+ - artifact type (class)
116
+ # * +params+ - optional map of artifact options
117
+ # * +blk+ - a block which will be passed to artifacts constructor;
118
+ # applications may vary but particular one is adding
119
+ # artifacts into composite during instantiation
120
+ def artifact(type, params = nil, &blk)
121
+ artifact_object = create_artifact_object(type, params, &blk)
122
+ self[artifact_object.label] = artifact_object
123
+ artifact_object.as_a_dependency.to_self
124
+ end
125
+
126
+ ##
127
+ # Put labels on the artifact
128
+ #
129
+ # Put the artifact into the artifact storage under arbitrary aliases.
130
+ #
131
+ # * +artifact_object+ - artifact object to put
132
+ # * +labels+ - labels to assign to the artifact
133
+ def label_as(artifact_object, *labels)
134
+ labels.flatten.each do |artifact_label|
135
+ self[artifact_label] = artifact_object
136
+ end
137
+
138
+ artifact_object
139
+ end
140
+
141
+ ##
142
+ # Provisions parameters and initializes nested artifacts
143
+ def initialize(*params, &blk)
144
+ super(*params)
145
+ create_components
146
+ instance_exec(&blk) if blk
147
+ end
148
+
149
+ ##
150
+ # Find artifacts by criteria
151
+ #
152
+ # The method allows flexible introspection of the artifacts
153
+ # enclosed into the composite's storage. The method is just a proxy
154
+ # for the storage method with the same name
155
+ #
156
+ # * +search_params+ - map of search parameters:
157
+ # ** +recursive+ - if true, search will be performed recusrsively
158
+ # in nested composites
159
+ # ** +label+ - search for artifacts which have the label
160
+ # ** +parameters+ - search for artifacts which have specified
161
+ # parameters values; it's a multi-level map so
162
+ # you can check for nested values also
163
+ def search(search_params = {})
164
+ artifacts.search(search_params)
165
+ end
166
+
167
+ protected
168
+
169
+ def create_components; end
170
+
171
+ private
172
+
173
+ def create_artifact_object(type, params, &blk)
174
+ type.new(options.filter(&contextualize(params.to_filter)), &blk)
175
+ end
176
+ end
177
+ end
178
+ end
@@ -0,0 +1,221 @@
1
+ module Aws
2
+ module Templates
3
+ ##
4
+ # Parameter definition exception
5
+ #
6
+ # Meta-programming exception related to Parametrized DSL
7
+ class ParametrizedDSLError < StandardError
8
+ end
9
+
10
+ ##
11
+ # Parameter already exists
12
+ #
13
+ # If you're trying to define a parameter in a parametrized artifact
14
+ # and this parameter either already defined for the class or defined
15
+ # in an ancestor.
16
+ class ParameterAlreadyExist < ParametrizedDSLError
17
+ # Parameter object of the conflicting parameter
18
+ attr_reader :parameter
19
+
20
+ def initialize(target_parameter)
21
+ @parameter = target_parameter
22
+ super(
23
+ "Parameter #{target_parameter.name} already in " \
24
+ "#{target_parameter.klass}."
25
+ )
26
+ end
27
+ end
28
+
29
+ ##
30
+ # Invalid parameter specification hash
31
+ #
32
+ # If unknown option is passed in a parameter description block
33
+ class ParameterSpecificationIsInvalid < ParametrizedDSLError
34
+ # Parameter object faulty options were specified for
35
+ attr_reader :parameter
36
+
37
+ # Options unknown to Parametrized
38
+ attr_reader :options
39
+
40
+ def initialize(target_parameter, opts)
41
+ @parameter = target_parameter
42
+ @options = opts
43
+
44
+ super(
45
+ 'Unsupported options are in specification for ' \
46
+ "parameter #{target_parameter.name} in class " \
47
+ "#{target_parameter.klass} : #{opts}"
48
+ )
49
+ end
50
+ end
51
+
52
+ ##
53
+ # A regular method and a parameter have the same name in a class
54
+ #
55
+ # A parameter was specified with the same name as exsiting method
56
+ # in the class or in an ancestor of the class.
57
+ class ParameterMethodNameConflict < ParametrizedDSLError
58
+ # Method object of the method specified
59
+ attr_reader :method_object
60
+
61
+ def initialize(target_method)
62
+ @method_object = target_method
63
+
64
+ super(
65
+ "Parameter name #{target_method.name} clashes with a method name in " \
66
+ "#{target_method.owner.name}"
67
+ )
68
+ end
69
+ end
70
+
71
+ ##
72
+ # View was not found for the object
73
+ #
74
+ # View map was checked and there is no appropriate view class
75
+ # for the object class found in the registry.
76
+ class ViewNotFound < RuntimeError
77
+ # Instance of the object class render lookup was performed for
78
+ attr_reader :instance
79
+
80
+ def message
81
+ "Can't find any view for #{instance} of class #{instance.class}"
82
+ end
83
+
84
+ def initialize(target_instance)
85
+ super()
86
+ @instance = target_instance
87
+ end
88
+ end
89
+
90
+ ##
91
+ # Parameter exception
92
+ #
93
+ # Happens during runtime if an error happens during parameter
94
+ # evaluation
95
+ class ParameterException < RuntimeError
96
+ # Parameter object
97
+ attr_reader :parameter
98
+
99
+ def message
100
+ cause.nil? ? super : "#{super} : #{cause.message}"
101
+ end
102
+
103
+ def initialize(target_parameter, custom_message)
104
+ @parameter = target_parameter
105
+ super(custom_message)
106
+ end
107
+ end
108
+
109
+ ##
110
+ # If something happens during parameter calculation
111
+ class NestedParameterException < ParameterException
112
+ def initialize(target_parameter)
113
+ super(
114
+ target_parameter,
115
+ 'Exception was thrown by nested parameter while calculating ' \
116
+ "#{target_parameter.name} (#{target_parameter.description})"
117
+ )
118
+ end
119
+ end
120
+
121
+ ##
122
+ # A value failed constraints
123
+ class ParameterValueInvalid < ParameterException
124
+ attr_reader :value
125
+ attr_reader :object
126
+
127
+ def initialize(target_parameter, target_object, target_value)
128
+ @value = target_value
129
+ @object = target_object
130
+ super(
131
+ target_parameter,
132
+ message_text(target_parameter, target_object, target_value)
133
+ )
134
+ end
135
+
136
+ private
137
+
138
+ def message_text(target_parameter, target_object, target_value)
139
+ message = "Value '(#{target_value.inspect})' violates constraints specified for " \
140
+ "#{target_parameter.name} (#{target_parameter.description}) in " \
141
+ "#{target_parameter.klass}"
142
+
143
+ unless target_object.class == target_parameter.klass
144
+ message += " and inherited by #{target_object.class}"
145
+ end
146
+
147
+ message
148
+ end
149
+ end
150
+
151
+ ##
152
+ # Getter is not specified
153
+ #
154
+ # Getter wasn't specified neither for the individual parameter nor for the mixing instance nor
155
+ # for its class.
156
+ class ParameterGetterIsNotDefined < ParameterException
157
+ def initialize(target_parameter)
158
+ super(
159
+ target_parameter,
160
+ "Can't find getter for #{target_parameter.name} (#{target_parameter.description}): " \
161
+ 'a getter should be attached either to the parameter or the instance ' \
162
+ 'or the instance class'
163
+ )
164
+ end
165
+ end
166
+
167
+ ##
168
+ # Options exception
169
+ #
170
+ # The parent of all exceptions Options method can throw
171
+ class OptionError < ArgumentError
172
+ end
173
+
174
+ ##
175
+ # Recursive value is expected
176
+ #
177
+ # Value passed doesn't not support "recursive" contract. See Utils.recursive?
178
+ class OptionShouldBeRecursive < OptionError
179
+ attr_reader :value
180
+
181
+ def initialize(value)
182
+ @value = value
183
+ super("Value #{value} is not a recursive data structure")
184
+ end
185
+ end
186
+
187
+ ##
188
+ # Deleted branch detected
189
+ #
190
+ # While traversing Options layers for a value, deleted branch marker was discovered.
191
+ class OptionValueDeleted < OptionError
192
+ attr_reader :path
193
+
194
+ def initialize(path)
195
+ @path = path
196
+ super(
197
+ "Deleted value was detected while traversing path. The path left untraversed: #{path}"
198
+ )
199
+ end
200
+ end
201
+
202
+ ##
203
+ # Scalar is met while traversing Options path
204
+ #
205
+ # Path is not empty yet but we can't traverse deeper because the current value is a scalar
206
+ class OptionScalarOnTheWay < OptionError
207
+ attr_reader :value
208
+ attr_reader :path
209
+
210
+ def initialize(value, path)
211
+ @value = value
212
+ @path = path
213
+
214
+ super(
215
+ "Value #{value} is not a recursive data structure and we have still #{path} keys " \
216
+ 'to look-up'
217
+ )
218
+ end
219
+ end
220
+ end
221
+ end
@@ -0,0 +1,60 @@
1
+ require 'aws/templates/exceptions'
2
+
3
+ module Aws
4
+ module Templates
5
+ module Render
6
+ ##
7
+ # View registry
8
+ #
9
+ # View registries encapsulate differerent ways of transforming
10
+ # your artifacts into a domain-specific output.
11
+ # In nutshell, they are registries of View classes which are able
12
+ # to lookup proper View for object instance passed to it.
13
+ class Registry
14
+ # View registry accessor
15
+ def registry
16
+ @registry ||= {}
17
+ end
18
+
19
+ ##
20
+ # Register pair artifact-view
21
+ #
22
+ # Invoked from inside of a View class at definition of the link
23
+ # between the view class and an artifact
24
+ # * +artifact+ - artifact class the view claims to be able to render
25
+ # * +render+ - view class
26
+ def register(artifact, view)
27
+ registry[artifact] = view
28
+ end
29
+
30
+ ##
31
+ # Can object be rendered
32
+ #
33
+ # Returns true if the object passed can be rendered by one of the views in the registry
34
+ def can_render?(instance)
35
+ instance.class.ancestors.any? { |ancestor| registry.include?(ancestor) }
36
+ end
37
+
38
+ ##
39
+ # Lookup a view for the artifact
40
+ #
41
+ # Searches registry for artifact's class and all its ancestors
42
+ # in the registry and returns the closest matching view
43
+ # * +instance+ - artifact instance to render
44
+ # * +params+ - assigned parameters; it can be arbitrary value;
45
+ # it is propagated to selected render
46
+ def view_for(instance, params = nil)
47
+ return instance if instance.respond_to?(:to_rendered)
48
+
49
+ mod = instance.class.ancestors.find do |ancestor|
50
+ registry.include?(ancestor)
51
+ end
52
+
53
+ raise ViewNotFound.new(instance) unless mod
54
+
55
+ registry[mod].new(instance, params)
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end