praxis-blueprints 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 37262e51d6ade8fa158c9d9f6de290736a0b3a51
4
+ data.tar.gz: a6a308d7b28643974c4226153be1a0414a84084d
5
+ SHA512:
6
+ metadata.gz: 850cecb7dbc21a95ec11b7a055bcdb21238d436bcf9c51d26901d3c371fc507629d2ebe22afccdc21f75d855c9260afc696b51fdbc2bfee9c3936a1d7fefdcca
7
+ data.tar.gz: 50745d11696618b5d276415608d4e7a6e25815d75d272abb6525e8d40ff00e71dde1afa782a51fae307417b92aa873e374ae0103a218ab37e5d68324ce25e979
data/.gitignore ADDED
@@ -0,0 +1,28 @@
1
+ *.swp
2
+ *.swo
3
+
4
+ # YARD
5
+ .yardoc
6
+
7
+ # Gemfile
8
+ *.gem
9
+
10
+ # Code coverage
11
+ coverage
12
+
13
+ # RConf
14
+ .ruby-version
15
+
16
+ # Bundler
17
+ .bundle
18
+
19
+ # Mac OSX files
20
+ .DS_Store
21
+
22
+ bin
23
+
24
+ .idea
25
+
26
+ tmp/
27
+
28
+ Gemfile.lock
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --colour
2
+ --format=Fuubar
3
+ --backtrace
data/.travis.yml ADDED
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - "2.1.2"
4
+ script: bundle exec rspec spec
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ # praxis-blueprints changelog
2
+
3
+ ## 1.0
4
+
5
+ Initial release!
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
data/Guardfile ADDED
@@ -0,0 +1,11 @@
1
+ # Config file for Guard
2
+ # More info at https://github.com/guard/guard#readme
3
+
4
+ guard :rspec, cmd: 'bundle exec rspec' do
5
+ watch(%r{^spec/.+_spec\.rb$})
6
+ watch(%r{^lib/praxis-blueprints/(.+)\.rb$}) { |m| "spec/praxis-blueprints/#{m[1]}_spec.rb" }
7
+ watch('spec/*.rb') { 'spec' }
8
+ watch('lib/praxis-blueprints.rb') { 'spec' }
9
+ watch(%r{^spec/support/(.+)\.rb$}) { 'spec' }
10
+ end
11
+
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 RightScale
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,36 @@
1
+ # Praxis Blueprints
2
+
3
+ Praxis Blueprints is a library that allows for defining a reusable class structures that has a set of typed attributes and a set of views with which to render them. Instantiations of Blueprints resemble ruby Structs which respond to methods of the attribute names. Rendering is format-agnostic in that
4
+ it results in a structured hash instead of an encoded string. Blueprints can automatically generate object structures that follow the attribute definitions.
5
+
6
+ ## Installation
7
+
8
+ Add this line to your application's Gemfile:
9
+
10
+ gem 'praxis-blueprints'
11
+
12
+ And then execute:
13
+
14
+ $ bundle
15
+
16
+ Or install it yourself as:
17
+
18
+ $ gem install praxis-blueprints
19
+
20
+ ## Usage
21
+
22
+ Documentation coming soon.
23
+
24
+ ## Contributing
25
+
26
+ 1. Fork it ( https://github.com/[my-github-username]/praxis-blueprints/fork )
27
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
28
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
29
+ 4. Push to the branch (`git push origin my-new-feature`)
30
+ 5. Create a new Pull Request
31
+
32
+ ## License
33
+
34
+ This software is released under the [MIT License](http://www.opensource.org/licenses/MIT). Please see [LICENSE](LICENSE) for further details.
35
+
36
+ Copyright (c) 2014 RightScale
data/Rakefile ADDED
@@ -0,0 +1,16 @@
1
+ require 'bundler/setup'
2
+
3
+ require "bundler/gem_tasks"
4
+
5
+ require 'rspec/core'
6
+ require 'rspec/core/rake_task'
7
+
8
+ desc "Run RSpec code examples with simplecov"
9
+ RSpec::Core::RakeTask.new do |spec|
10
+ spec.pattern = FileList['spec/**/*_spec.rb']
11
+ end
12
+
13
+ task :default => :spec
14
+
15
+ require 'yard'
16
+ YARD::Rake::YardocTask.new
@@ -0,0 +1,330 @@
1
+ require 'ostruct'
2
+
3
+ # Blueprint ==
4
+ # - part implementation definition for attributes
5
+ # - part container for views
6
+ module Praxis
7
+ class Blueprint
8
+ include Attributor::Type
9
+ extend Finalizable
10
+
11
+ @@caching_enabled = false
12
+
13
+ CIRCULAR_REFERENCE_MARKER = '...'.freeze
14
+
15
+ attr_accessor :object, :decorators
16
+ attr_reader :validating, :active_renders
17
+
18
+ class << self
19
+ attr_reader :views, :attribute, :options
20
+ attr_accessor :reference
21
+ end
22
+
23
+ def self.inherited(klass)
24
+ super
25
+
26
+ klass.instance_eval do
27
+ @views = Hash.new
28
+ @options = Hash.new
29
+ end
30
+ end
31
+
32
+ # Override default new behavior to support memoized creation through an IdentityMap
33
+ def self.new(object, decorators=nil)
34
+ if @@caching_enabled && decorators.nil?
35
+ key = object
36
+
37
+ cache = if object.respond_to?(:identity_map)
38
+ object.identity_map.blueprint_cache[self]
39
+ else
40
+ self.cache
41
+ end
42
+
43
+ return cache[key] ||= begin
44
+ blueprint = self.allocate
45
+ blueprint.send(:initialize, object, decorators)
46
+ blueprint
47
+ end
48
+ end
49
+
50
+ blueprint = self.allocate
51
+ blueprint.send(:initialize, object, decorators)
52
+ blueprint
53
+ end
54
+
55
+
56
+ def self.describe(shallow=false)
57
+ type_name = self.ancestors.find { |k| k.name && !k.name.empty? }.name
58
+
59
+ description = self.attribute.type.describe(shallow).merge!(name: type_name)
60
+
61
+ unless shallow
62
+ description[:views] = self.views.each_with_object({}) do |(view_name, view), hash|
63
+ hash[view_name] = view.describe
64
+ end
65
+ end
66
+
67
+ description
68
+ end
69
+
70
+
71
+ def self.attributes(opts={}, &block)
72
+ if block_given?
73
+ if self.const_defined?(:Struct, false)
74
+ raise "Redefining Blueprint attributes is not currently supported"
75
+ else
76
+
77
+ if opts.has_key?(:reference) && opts[:reference] != self.reference
78
+ raise "Reference mismatch in #{self.inspect}. Given :reference option #{opts[:reference].inspect}, while using #{self.reference.inspect}"
79
+ elsif self.reference
80
+ opts[:reference] = self.reference #pass the reference Class down
81
+ else
82
+ opts[:reference] = self
83
+ end
84
+
85
+ @options = opts
86
+ @block = block
87
+ end
88
+
89
+ return @attribute
90
+ end
91
+
92
+ unless @attribute
93
+ raise "@attribute not defined yet for #{self.name}"
94
+ end
95
+
96
+ @attribute.attributes
97
+ end
98
+
99
+
100
+ def self.check_option!(name, value)
101
+ case name
102
+ when :identity
103
+ raise Attributor::AttributorException, "Invalid identity type #{value.inspect}" unless value.kind_of?(::Symbol)
104
+ return :ok
105
+ else
106
+ return Attributor::Struct.check_option!(name, value)
107
+ end
108
+ end
109
+
110
+
111
+ def self.load(value,context=Attributor::DEFAULT_ROOT_CONTEXT, **options)
112
+ case value
113
+ when nil, self
114
+ value
115
+ when Hash, String
116
+ # Need to parse/deserialize first
117
+ self.new(self.attribute.load(value,context, **options))
118
+ else
119
+ # Just wrap whatever value
120
+ self.new(value)
121
+ end
122
+ end
123
+
124
+
125
+ def self.caching_enabled?
126
+ @@caching_enabled
127
+ end
128
+
129
+ def self.caching_enabled=(caching_enabled)
130
+ @@caching_enabled = caching_enabled
131
+ end
132
+
133
+ # Fetch current blueprint cache, scoped by this class
134
+ def self.cache
135
+ Thread.current[:praxis_blueprints_cache][self]
136
+ end
137
+
138
+ def self.cache=(cache)
139
+ Thread.current[:praxis_blueprints_cache] = cache
140
+ end
141
+
142
+ def self.valid_type?(value)
143
+ # FIXME: this should be more... ducklike
144
+ value.kind_of?(self) || value.kind_of?(self.attribute.type)
145
+ end
146
+
147
+ def self.example(context=nil, **values)
148
+ context = case context
149
+ when nil
150
+ ["#{self.name}-#{values.object_id.to_s}"]
151
+ when ::String
152
+ [context]
153
+ else
154
+ context
155
+ end
156
+
157
+ self.new(self.attribute.example(context, values: values))
158
+ end
159
+
160
+
161
+ def self.validate(value, context=Attributor::DEFAULT_ROOT_CONTEXT, _attribute=nil)
162
+
163
+ raise ArgumentError, "Invalid context received (nil) while validating value of type #{self.name}" if context == nil
164
+ context = [context] if context.is_a? ::String
165
+
166
+ unless value.kind_of?(self)
167
+ raise ArgumentError, "Error validating #{Attributor.humanize_context(context)} as #{self.name} for an object of type #{value.class.name}."
168
+ end
169
+
170
+ value.validate(context)
171
+ end
172
+
173
+
174
+ def self.view(name, &block)
175
+ if block_given?
176
+ return self.views[name] = View.new(name, self, &block)
177
+ end
178
+
179
+ self.views[name]
180
+ end
181
+
182
+ def self.dump(object, view: :default, context: Attributor::DEFAULT_ROOT_CONTEXT, **opts)
183
+ object = self.load(object, context)
184
+ object.render(view, context: context)
185
+ end
186
+
187
+
188
+ def initialize(object, decorators=nil)
189
+ # TODO: decide what sort of type checking (if any) we want to perform here.
190
+ @object = object
191
+ @decorators = if decorators.kind_of?(Hash) && decorators.any?
192
+ OpenStruct.new(decorators)
193
+ else
194
+ decorators
195
+ end
196
+ @rendered_views = {}
197
+ @validating = false
198
+
199
+ # OPTIMIZE: revisit the circular rendering tracking.
200
+ # removing this results in a significant performance
201
+ # and memory use savings.
202
+ @active_renders = []
203
+ end
204
+
205
+
206
+ # Render the wrapped data with the given view
207
+ def render(view_name=:default, context: Attributor::DEFAULT_ROOT_CONTEXT)
208
+ unless (view = self.class.views[view_name])
209
+ raise "view with name '#{view_name.inspect}' is not defined in #{self.class}"
210
+ end
211
+
212
+ return @rendered_views[view_name] if @rendered_views.has_key? view_name
213
+ return CIRCULAR_REFERENCE_MARKER if @active_renders.include?(view_name)
214
+ @active_renders << view_name
215
+
216
+ @rendered_views[view_name] = view.dump(self, context: context)
217
+ ensure
218
+ @active_renders.delete view_name
219
+ end
220
+
221
+
222
+ alias_method :to_hash, :render
223
+
224
+
225
+ def dump(view: :default, context: Attributor::DEFAULT_ROOT_CONTEXT)
226
+ self.render(view, context: context)
227
+ end
228
+
229
+ # Internal finalize! logic
230
+ def self._finalize!
231
+ if @block
232
+ self.define_attribute!
233
+ self.define_readers!
234
+ self.generate_master_view!
235
+ end
236
+ super
237
+ end
238
+
239
+ def self.define_attribute!
240
+ @attribute = Attributor::Attribute.new(Attributor::Struct, @options, &@block)
241
+ @block = nil
242
+ self.const_set(:Struct, @attribute.type)
243
+ end
244
+
245
+ def self.define_readers!
246
+ self.attributes.each do |name, attribute|
247
+ name = name.to_sym
248
+
249
+ # Don't redefine existing methods
250
+ next if self.instance_methods.include? name
251
+
252
+ define_reader! name
253
+ end
254
+ end
255
+
256
+
257
+ def self.define_reader!(name)
258
+ attribute = self.attributes[name]
259
+ if attribute.type < Praxis::Blueprint
260
+ define_blueprint_reader!(name)
261
+ else
262
+ define_direct_reader!(name)
263
+ end
264
+ end
265
+
266
+ def self.define_blueprint_reader!(name)
267
+ # it's faster to use define_method in this case than module_eval
268
+ # because we save the attribute lookup on every access.
269
+ attribute = self.attributes[name]
270
+ define_method(name) do
271
+ if @decorators && @decorators.respond_to?(name)
272
+ @decorators.send(name)
273
+ else
274
+ value = @object.send(name)
275
+ return value if value.nil? || value.kind_of?(attribute.type)
276
+ attribute.type.new(value)
277
+ end
278
+ end
279
+ end
280
+
281
+ def self.define_direct_reader!(name)
282
+ module_eval <<-RUBY, __FILE__, __LINE__ + 1
283
+ def #{name}
284
+ if @decorators && @decorators.respond_to?(:#{name})
285
+ @decorators.#{name}
286
+ else
287
+ @object.#{name}
288
+ end
289
+ end
290
+ RUBY
291
+ end
292
+
293
+ def self.generate_master_view!
294
+ attributes = self.attributes
295
+ view :master do
296
+ attributes.each do | name, attr |
297
+ # Note: we can freely pass master view for attributes that aren't blueprint/containers because
298
+ # their dump methods will ignore it (they always dump everything regardless)
299
+ attribute name, view: :master
300
+ end
301
+ end
302
+ end
303
+
304
+
305
+ def validate(context=Attributor::DEFAULT_ROOT_CONTEXT)
306
+
307
+ raise ArgumentError, "Invalid context received (nil) while validating value of type #{self.name}" if context == nil
308
+ context = [context] if context.is_a? ::String
309
+
310
+ raise "validation conflict" if @validating
311
+ @validating = true
312
+
313
+
314
+ self.class.attributes.each_with_object(Array.new) do |(sub_attribute_name, sub_attribute), errors|
315
+ sub_context = self.class.generate_subcontext(context,sub_attribute_name)
316
+ value = self.send(sub_attribute_name)
317
+
318
+ if value.respond_to?(:validating) # really, it's a thing with sub-attributes
319
+ next if value.validating
320
+ end
321
+ errors.push *sub_attribute.validate(value, sub_context)
322
+ end
323
+ ensure
324
+ @validating = false
325
+ end
326
+
327
+
328
+ end
329
+
330
+ end
@@ -0,0 +1,40 @@
1
+ module Praxis
2
+ class ConfigHash < BasicObject
3
+
4
+ attr_reader :hash
5
+
6
+ def self.from(hash={},&block)
7
+ self.new(hash,&block)
8
+ end
9
+
10
+ def initialize(hash={},&block)
11
+ @hash = hash
12
+ @block = block
13
+ end
14
+
15
+ def to_hash
16
+ self.instance_eval(&@block)
17
+ @hash
18
+ end
19
+
20
+ def method_missing(name, value, *rest, &block)
21
+ if (existing = @hash[name])
22
+ if block
23
+ existing << [value, block]
24
+ else
25
+ existing << value
26
+ rest.each do |v|
27
+ existing << v
28
+ end
29
+ end
30
+ else
31
+ if rest.any?
32
+ @hash[name] = [value] + rest
33
+ else
34
+ @hash[name] = value
35
+ end
36
+ end
37
+ end
38
+
39
+ end
40
+ end
@@ -0,0 +1,38 @@
1
+ module Praxis
2
+ module Finalizable
3
+
4
+
5
+ def self.extended(klass)
6
+ klass.module_eval do
7
+ @finalizable = Set.new
8
+ end
9
+ end
10
+
11
+ def inherited(base)
12
+ @finalizable << base
13
+ base.instance_variable_set(:@finalizable, @finalizable)
14
+ base.instance_variable_set(:@finalized, false)
15
+ end
16
+
17
+ def finalizable
18
+ @finalizable
19
+ end
20
+
21
+ def finalized?
22
+ @finalized
23
+ end
24
+
25
+ def _finalize!
26
+ @finalized = true
27
+ end
28
+
29
+ def finalize!
30
+ self.finalizable.reject(&:finalized?).each do |klass|
31
+ klass._finalize!
32
+ end
33
+
34
+ self.finalize! unless self.finalizable.all?(&:finalized?)
35
+ end
36
+
37
+ end
38
+ end
@@ -0,0 +1,3 @@
1
+ module Praxis
2
+ BLUEPRINTS_VERSION = "1.0.0"
3
+ end
@@ -0,0 +1,93 @@
1
+ module Praxis
2
+
3
+ class View
4
+ attr_reader :schema, :contents, :name
5
+
6
+
7
+ def initialize(name, schema, &block)
8
+ @name = name
9
+ @schema = schema
10
+ @contents = ::Hash.new
11
+ @block = block
12
+ end
13
+
14
+
15
+ def contents
16
+ if @block
17
+ self.instance_eval(&@block)
18
+ @block = nil
19
+ end
20
+
21
+ @contents
22
+ end
23
+
24
+
25
+ def dump(object, context: Attributor::DEFAULT_ROOT_CONTEXT,**opts)
26
+ self.contents.each_with_object({}) do |(name, (dumpable, dumpable_opts)), hash|
27
+ next unless object.respond_to?(name)
28
+
29
+ begin
30
+ value = object.send(name)
31
+ rescue => e
32
+ raise Attributor::DumpError, context: context, name: name, type: object.class, original_exception: e
33
+ end
34
+ next if value.nil?
35
+
36
+ # FIXME: this is such an ugly way to do this. Need attributor#67.
37
+ if dumpable.kind_of?(View)
38
+ new_context = context + [name]
39
+ hash[name] = dumpable.dump(value, context: new_context ,**(dumpable_opts||{}))
40
+ else
41
+ type = dumpable.type
42
+ if type.respond_to?(:attributes) || type.respond_to?(:member_attribute)
43
+ new_context = context + [name]
44
+ hash[name] = dumpable.dump(value, context: new_context ,**(dumpable_opts||{}))
45
+ else
46
+ hash[name] = value
47
+ end
48
+ end
49
+ end
50
+ end
51
+ alias_method :to_hash, :dump
52
+
53
+
54
+ def attribute(name, opts={}, &block)
55
+ raise AttributorException, "Attribute names must be symbols, got: #{name.inspect}" unless name.kind_of? ::Symbol
56
+
57
+ attribute = self.schema.attributes.fetch(name) do
58
+ raise "Attribute '#{name}' does not exist in #{self.schema}"
59
+ end
60
+
61
+ if block_given?
62
+ view = View.new(name, attribute, &block)
63
+ @contents[name] = view
64
+ else
65
+ raise "Invalid options (#{opts.inspect}) for #{name} while defining view #{@name}" unless opts.is_a?(Hash)
66
+ @contents[name] = [attribute, opts]
67
+ end
68
+
69
+ end
70
+
71
+
72
+ def example(context=nil)
73
+ object = self.schema.example(context)
74
+ opts = {}
75
+ opts[:context] = context if context
76
+ self.dump(object, opts)
77
+ end
78
+
79
+ def describe
80
+ # TODO: for now we are just return the first level keys
81
+ view_attributes = {}
82
+
83
+ self.contents.each do |k,(dumpable,dumpable_opts)|
84
+ inner_desc = {}
85
+ inner_desc[:view] = dumpable_opts[:view] if dumpable_opts && dumpable_opts[:view]
86
+ view_attributes[k] = inner_desc
87
+ end
88
+
89
+ { attributes: view_attributes }
90
+ end
91
+
92
+ end
93
+ end
@@ -0,0 +1,14 @@
1
+ require 'json'
2
+ require 'yaml'
3
+ require 'logger'
4
+
5
+ require 'attributor'
6
+
7
+ require "praxis-blueprints/version"
8
+
9
+ require 'praxis-blueprints/finalizable'
10
+ require 'praxis-blueprints/config_hash'
11
+
12
+ require 'praxis-blueprints/blueprint'
13
+ require 'praxis-blueprints/view'
14
+