praxis-blueprints 1.0.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.
- checksums.yaml +7 -0
- data/.gitignore +28 -0
- data/.rspec +3 -0
- data/.travis.yml +4 -0
- data/CHANGELOG.md +5 -0
- data/Gemfile +3 -0
- data/Guardfile +11 -0
- data/LICENSE +22 -0
- data/README.md +36 -0
- data/Rakefile +16 -0
- data/lib/praxis-blueprints/blueprint.rb +330 -0
- data/lib/praxis-blueprints/config_hash.rb +40 -0
- data/lib/praxis-blueprints/finalizable.rb +38 -0
- data/lib/praxis-blueprints/version.rb +3 -0
- data/lib/praxis-blueprints/view.rb +93 -0
- data/lib/praxis-blueprints.rb +14 -0
- data/praxis-blueprints.gemspec +39 -0
- data/spec/praxis-blueprints/blueprint_spec.rb +353 -0
- data/spec/praxis-blueprints/view_spec.rb +316 -0
- data/spec/spec_helper.rb +40 -0
- data/spec/support/spec_blueprints.rb +76 -0
- metadata +270 -0
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
data/.travis.yml
ADDED
data/CHANGELOG.md
ADDED
data/Gemfile
ADDED
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,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
|
+
|