praxis 0.17.1 → 0.18.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 (44) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +1 -0
  3. data/CHANGELOG.md +18 -0
  4. data/lib/api_browser/package.json +1 -1
  5. data/lib/praxis/action_definition.rb +119 -14
  6. data/lib/praxis/api_general_info.rb +21 -3
  7. data/lib/praxis/application.rb +1 -0
  8. data/lib/praxis/dispatcher.rb +18 -15
  9. data/lib/praxis/docs/generator.rb +208 -0
  10. data/lib/praxis/handlers/www_form.rb +2 -1
  11. data/lib/praxis/media_type.rb +1 -1
  12. data/lib/praxis/multipart/part.rb +58 -6
  13. data/lib/praxis/request_stages/action.rb +10 -8
  14. data/lib/praxis/request_stages/request_stage.rb +2 -2
  15. data/lib/praxis/resource_definition.rb +11 -3
  16. data/lib/praxis/response_definition.rb +49 -25
  17. data/lib/praxis/restful_doc_generator.rb +9 -1
  18. data/lib/praxis/route.rb +16 -0
  19. data/lib/praxis/router.rb +1 -1
  20. data/lib/praxis/simple_media_type.rb +2 -1
  21. data/lib/praxis/tasks/api_docs.rb +10 -1
  22. data/lib/praxis/tasks/console.rb +10 -3
  23. data/lib/praxis/types/media_type_common.rb +8 -1
  24. data/lib/praxis/types/multipart.rb +5 -0
  25. data/lib/praxis/types/multipart_array.rb +12 -4
  26. data/lib/praxis/version.rb +1 -1
  27. data/lib/praxis.rb +3 -0
  28. data/praxis.gemspec +3 -3
  29. data/spec/functional_spec.rb +3 -3
  30. data/spec/praxis/action_definition_spec.rb +40 -0
  31. data/spec/praxis/api_general_info_spec.rb +10 -3
  32. data/spec/praxis/media_type_collection_spec.rb +11 -4
  33. data/spec/praxis/media_type_spec.rb +3 -1
  34. data/spec/praxis/request_stages/action_spec.rb +13 -6
  35. data/spec/praxis/resource_definition_spec.rb +1 -1
  36. data/spec/praxis/response_definition_spec.rb +29 -3
  37. data/spec/praxis/router_spec.rb +1 -1
  38. data/spec/praxis/types/multipart_array_spec.rb +92 -0
  39. data/spec/praxis/types/multipart_spec.rb +5 -0
  40. data/spec/spec_app/design/api.rb +1 -0
  41. data/spec/spec_app/design/media_types/volume.rb +1 -3
  42. data/spec/spec_app/design/resources/instances.rb +1 -1
  43. data/spec/support/spec_media_types.rb +5 -4
  44. metadata +10 -9
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: bff7fa716d64ecf217102ec4c36cb53c052621ab
4
- data.tar.gz: 372c8c5e252e0bff274a3c0d5f105d16deb515cc
3
+ metadata.gz: 5c55f5d3363aed4bc5157e0064e35b399e1bcf20
4
+ data.tar.gz: 2bf66b28b0d80ea8e57de43b69dc5c76e7e504ea
5
5
  SHA512:
6
- metadata.gz: d2c4326ada2a6b7bc8b72ab28ca0cd87ee945a8086f096511accd18585d060f7304bc06649ed9abbff3d970e912960e069f8ec73e4ef0888140ca075fc8568ac
7
- data.tar.gz: f59b8ce34535a3294523f947abcda452b6b587c31c31a0a7c02c1cf19bb4a826c1cfbce37cd092cbe137900c0b8fc539761eefd62ca1e679ea0721a842101bf1
6
+ metadata.gz: 32fdd9a038fa46b129d1da10a9edee94d032c2ff7ce14889cf45f162b57e3d2aaf4d3ecc4f5827ddb84a2bc33490259293a4b84a3905cd2dd269f1fb0a2a29c2
7
+ data.tar.gz: 04b7c664a294d7766e65640855112fb80455f6ba3fe7c840bb5b70e5de4d8bcf13b58f8cefaf151ea3787644185c4e20070f5f5cb2edcf2c58d78822ca2fef63
data/.travis.yml CHANGED
@@ -1,5 +1,6 @@
1
1
  language: ruby
2
2
  cache: bundler
3
+ sudo: false
3
4
  rvm:
4
5
  - 2.1.5
5
6
  - 2.2.2
data/CHANGELOG.md CHANGED
@@ -2,6 +2,23 @@
2
2
 
3
3
  ## next
4
4
 
5
+ ## 0.18.0
6
+
7
+ * Added `display_name` DSL to `ResourceDefinition` and `MediaType`
8
+ * It is a purely informational field, mostly to be used by consumers of the generated docs
9
+ * It defaults to the class name (stripping any of the prefix modules)
10
+ * Revamped document generation to output a more compact format:
11
+ * 1 file per api version: including info, resources, schemas and traits.
12
+ * 1 single index file with global info plus just a list of version names
13
+ * new task currently usable through `bundle exec rake praxis:docs:generate_beta`
14
+ * NOTE: leaves the current doc generation tasks and code intact (until the doc browser is switched to use this)
15
+ * Specialized `Multipart`’s family in its description to be ‘multipart’ instead of ‘hash’.
16
+ * Added `Praxis::Handlers::FormData` for 'multipart/form-data'. Currently returns the input unchanged in `parse` and `generate`.
17
+ * Added `Praxis::Handlers::WWWForm` for form-encoded data.
18
+ * Added `Docs::Generator`, experimental new documentation generator. Use the `praxis:docs:experiments` rake task to generate. *Note*: not currently compatible with the documentation browser.
19
+ * Added 'praxis.request_stage.execute' `ActiveSupport::Notifications` instrumentation to contorller action method execution in `RequestStages::Action#execute`.
20
+ * Make action headers, params and payloads be required by default as it is probably what most people expect from it. To make any of those three definitions non-required, simply add the `:required` option as used in any other attribute definition. For example: `payload required: false do ...`
21
+
5
22
  ## 0.17.1
6
23
 
7
24
  * Fixes an issue that would make exported documentation broken.
@@ -47,6 +64,7 @@ config option, and uses the `media_type` defined for the response definition.
47
64
  * Adds hierarchival navigation to the doc browser.
48
65
  * Adds a ConfigurationProvider allowing for easy doc customization.
49
66
 
67
+
50
68
  ## 0.16.1
51
69
 
52
70
  * Fixed a bug where documentation generation would fail if an application had headers in a Trait using the simplified `header` DSL.
@@ -23,7 +23,7 @@
23
23
  "grunt-file-blocks": "^0.3.0",
24
24
  "grunt-filerev": "^0.2.1",
25
25
  "grunt-ng-annotate": "^0.10.0",
26
- "grunt-sass": "^0.18.0",
26
+ "grunt-sass": "^1.0.0",
27
27
  "grunt-usemin": "^2.1.1",
28
28
  "grunt-wiredep": "^1.7.0",
29
29
  "load-grunt-tasks": "^0.4.0"
@@ -67,7 +67,7 @@ module Praxis
67
67
  unless ApiDefinition.instance.traits.has_key? trait_name
68
68
  raise Exceptions::InvalidTrait.new("Trait #{trait_name} not found in the system")
69
69
  end
70
-
70
+
71
71
  trait = ApiDefinition.instance.traits.fetch(trait_name)
72
72
  trait.apply!(self)
73
73
  traits << trait_name
@@ -82,7 +82,7 @@ module Praxis
82
82
  def response(name, type=nil, **args, &block)
83
83
  if type
84
84
  # should verify type is a media type
85
-
85
+
86
86
  if block_given?
87
87
  type = type.construct(block)
88
88
  end
@@ -103,7 +103,11 @@ module Praxis
103
103
  end
104
104
 
105
105
  def params(type=Attributor::Struct, **opts, &block)
106
- return @params if !block && type == Attributor::Struct
106
+ return @params if !block && ( opts.nil? || opts.empty? ) && type == Attributor::Struct
107
+
108
+ unless( opts.key? :required )
109
+ opts[:required] = true # Make the payload required by default
110
+ end
107
111
 
108
112
  if @params
109
113
  unless type == Attributor::Struct && @params.type < Attributor::Struct
@@ -128,7 +132,11 @@ module Praxis
128
132
  end
129
133
 
130
134
  def payload(type=Attributor::Struct, **opts, &block)
131
- return @payload if !block && type == Attributor::Struct
135
+ return @payload if !block && ( opts.nil? || opts.empty? ) && type == Attributor::Struct
136
+
137
+ unless( opts.key? :required )
138
+ opts[:required] = true # Make the payload required by default
139
+ end
132
140
 
133
141
  if @payload
134
142
  unless type == Attributor::Struct && @payload.type < Attributor::Struct
@@ -144,6 +152,11 @@ module Praxis
144
152
 
145
153
  def headers(type=nil, **opts, &block)
146
154
  return @headers unless block
155
+
156
+ unless( opts.key? :required )
157
+ opts[:required] = true # Make the payload required by default
158
+ end
159
+
147
160
  if @headers
148
161
  update_attribute(@headers, opts, block)
149
162
  else
@@ -188,32 +201,56 @@ module Praxis
188
201
  @description
189
202
  end
190
203
 
204
+ def self.url_description(route:, params_example:, params:)
205
+ route_description = route.describe
206
+
207
+ example_hash = params_example ? params_example.dump : {}
208
+ hash = route.example(example_hash: example_hash, params: params)
209
+
210
+ query_string = URI.encode_www_form(hash[:query_params])
211
+ url = hash[:url]
212
+ url = [url,query_string].join('?') unless query_string.empty?
213
+
214
+ route_description[:example] = url
215
+ route_description
216
+ end
191
217
 
192
- def describe
218
+ def describe(context: nil)
193
219
  {}.tap do |hash|
194
220
  hash[:description] = description
195
221
  hash[:name] = name
196
222
  hash[:metadata] = metadata
197
- # FIXME: change to :routes along with api browser
198
- hash[:urls] = routes.collect(&:describe)
199
- hash[:headers] = headers.describe if headers
223
+ if headers
224
+ headers_example = headers.example(context)
225
+ hash[:headers] = headers.describe(example: headers_example)
226
+ end
200
227
  if params
201
- hash[:params] = params_description
228
+ params_example = params.example(context)
229
+ hash[:params] = params_description(example: params_example)
202
230
  end
203
- hash[:payload] = payload.describe if payload
231
+ if payload
232
+ payload_example = payload.example(context)
233
+
234
+ hash[:payload] = payload_description(example: payload_example)
235
+ end
236
+
204
237
  hash[:responses] = responses.inject({}) do |memo, (response_name, response)|
205
- memo[response.name] = response.describe
238
+ memo[response.name] = response.describe(context: context)
206
239
  memo
207
240
  end
208
241
  hash[:traits] = traits if traits.any?
209
-
242
+ # FIXME: change to :routes along with api browser
243
+ hash[:urls] = routes.collect do |route|
244
+ ActionDefinition.url_description(route: route, params: self.params, params_example: params_example)
245
+ end.compact
210
246
  self.class.doc_decorations.each do |callback|
211
247
  callback.call(self, hash)
212
248
  end
213
249
  end
214
250
  end
215
251
 
216
- def params_description
252
+
253
+ def params_description(example:)
217
254
  route_params = []
218
255
  if primary_route.nil?
219
256
  warn "Warning: No routes defined for #{resource_definition.name}##{name}."
@@ -224,7 +261,7 @@ module Praxis
224
261
  collect(&:to_sym)
225
262
  end
226
263
 
227
- desc = params.describe
264
+ desc = params.describe(example: example)
228
265
  desc[:type][:attributes].keys.each do |k|
229
266
  source = if route_params.include? k
230
267
  'url'
@@ -236,6 +273,74 @@ module Praxis
236
273
  desc
237
274
  end
238
275
 
276
+ # Determine the content_type to report for a given example,
277
+ # using handler_name if possible.
278
+ #
279
+ # Considers any pre-defined set of values on the content_type attributge
280
+ # of the headers.
281
+ def derive_content_type(example, handler_name)
282
+ # MultipartArrays *must* use the provided content_type
283
+ if example.kind_of? Praxis::Types::MultipartArray
284
+ return MediaTypeIdentifier.load(example.content_type)
285
+ end
286
+
287
+ _, content_type_attribute = self.headers && self.headers.attributes.find { |k,v| k.to_s =~ /^content[-_]{1}type$/i }
288
+ if content_type_attribute && content_type_attribute.options.key?(:values)
289
+
290
+ # if any defined value match the preferred handler_name, return it
291
+ content_type_attribute.options[:values].each do |ct|
292
+ mti = MediaTypeIdentifier.load(ct)
293
+ return mti if mti.handler_name == handler_name
294
+ end
295
+
296
+ # otherwise, pick the first
297
+ pick = MediaTypeIdentifier.load(content_type_attribute.options[:values].first)
298
+
299
+ # and return that one if it already corresponds to a registered handler
300
+ # otherwise, add the encoding
301
+ if Praxis::Application.instance.handlers.include?(pick.handler_name)
302
+ return pick
303
+ else
304
+ return pick + handler_name
305
+ end
306
+ end
307
+
308
+ # generic default encoding
309
+ MediaTypeIdentifier.load("application/#{handler_name}")
310
+ end
311
+
312
+
313
+ def payload_description(example:)
314
+ hash = payload.describe(example: example)
315
+
316
+ hash[:examples] = {}
317
+
318
+ default_handlers = ApiDefinition.instance.info.consumes
319
+
320
+ default_handlers.each do |default_handler|
321
+ dumped_payload = payload.dump(example, default_format: default_handler)
322
+
323
+ content_type = derive_content_type(example, default_handler)
324
+ handler = Praxis::Application.instance.handlers[content_type.handler_name]
325
+
326
+ # in case handler is nil, use dumped_payload as-is.
327
+ generated_payload = if handler.nil?
328
+ dumped_payload
329
+ else
330
+ handler.generate(dumped_payload)
331
+ end
332
+
333
+
334
+ hash[:examples][default_handler] = {
335
+ content_type: content_type.to_s,
336
+ body: generated_payload
337
+ }
338
+ end
339
+
340
+ hash
341
+ end
342
+
343
+
239
344
  def nodoc!
240
345
  metadata[:doc_visibility] = :none
241
346
  end
@@ -10,8 +10,9 @@ module Praxis
10
10
 
11
11
  if @global_info.nil? # this *is* the global info
12
12
  version_with [:header, :params]
13
+ consumes 'json', 'x-www-form-urlencoded'
14
+ produces 'json'
13
15
  end
14
-
15
16
  end
16
17
 
17
18
  def get(k)
@@ -97,6 +98,24 @@ module Praxis
97
98
  end
98
99
  end
99
100
 
101
+ def consumes(*vals)
102
+ if vals.empty?
103
+ return get(:consumes)
104
+ else
105
+ return set(:consumes, vals)
106
+ end
107
+ end
108
+
109
+
110
+ def produces(*vals)
111
+ if vals.empty?
112
+ return get(:produces)
113
+ else
114
+ return set(:produces, vals)
115
+ end
116
+ end
117
+
118
+
100
119
  def base_params(type=Attributor::Struct, **opts, &block)
101
120
  if !block && type == Attributor::Struct
102
121
  get(:base_params)
@@ -105,10 +124,9 @@ module Praxis
105
124
  end
106
125
  end
107
126
 
108
-
109
127
  def describe
110
128
  hash = { schema_version: "1.0".freeze }
111
- [:name, :title, :description, :base_path, :version_with, :endpoint].each do |attr|
129
+ [:name, :title, :description, :base_path, :version_with, :endpoint, :consumes, :produces].each do |attr|
112
130
  val = self.__send__(attr)
113
131
  hash[attr] = val unless val.nil?
114
132
  end
@@ -93,6 +93,7 @@ module Praxis
93
93
  #
94
94
  # @param [String] name
95
95
  # @param [Class] a class that responds to .new, #parse and #generate
96
+
96
97
  def handler(name, handler)
97
98
  # Construct an instance, if the handler is a class and needs to be initialized.
98
99
  handler = handler.new
@@ -79,25 +79,27 @@ module Praxis
79
79
  payload = {request: request, response: nil}
80
80
 
81
81
  Notifications.instrument 'praxis.request.all'.freeze, payload do
82
- # the response stage must be the final stage in the list
83
- *stages, response_stage = @stages
84
-
85
- stages.each do |stage|
86
- result = stage.run
87
- case result
88
- when Response
89
- controller.response = result
90
- break
82
+ begin
83
+ # the response stage must be the final stage in the list
84
+ *stages, response_stage = @stages
85
+
86
+ stages.each do |stage|
87
+ result = stage.run
88
+ case result
89
+ when Response
90
+ controller.response = result
91
+ break
92
+ end
91
93
  end
92
- end
93
94
 
94
- response_stage.run
95
+ response_stage.run
95
96
 
96
- payload[:response] = controller.response
97
- controller.response.finish
97
+ payload[:response] = controller.response
98
+ controller.response.finish
99
+ rescue => e
100
+ @application.error_handler.handle!(request, e)
101
+ end
98
102
  end
99
- rescue => e
100
- @application.error_handler.handle!(request, e)
101
103
  ensure
102
104
  @controller = nil
103
105
  @action = nil
@@ -105,6 +107,7 @@ module Praxis
105
107
  end
106
108
 
107
109
 
110
+ # TODO: fix for multithreaded environments
108
111
  def reset_cache!
109
112
  return unless Praxis::Blueprint.caching_enabled?
110
113
 
@@ -0,0 +1,208 @@
1
+ module Praxis
2
+ module Docs
3
+
4
+ class Generator
5
+ API_DOCS_DIRNAME = 'docs/api'
6
+
7
+ attr_reader :resources_by_version, :types_by_id, :infos_by_version
8
+ attr_reader :doc_root_dir
9
+
10
+ EXCLUDED_TYPES_FROM_OUTPUT = Set.new([
11
+ Attributor::Boolean,
12
+ Attributor::CSV,
13
+ Attributor::DateTime,
14
+ Attributor::Float,
15
+ Attributor::Hash,
16
+ Attributor::Ids,
17
+ Attributor::Integer,
18
+ Attributor::Object,
19
+ Attributor::String,
20
+ Attributor::Symbol
21
+ ]).freeze
22
+
23
+
24
+ def initialize(root)
25
+ require 'yaml'
26
+ @resources_by_version = Hash.new do |h,k|
27
+ h[k] = Set.new
28
+ end
29
+ initialize_directories(root)
30
+
31
+ Attributor::AttributeResolver.current = Attributor::AttributeResolver.new
32
+ collect_infos
33
+ collect_resources
34
+ collect_types
35
+ end
36
+
37
+ def save!
38
+ write_index_file
39
+ resources_by_version.keys.each do |version|
40
+ write_version_file(version)
41
+ end
42
+ end
43
+
44
+ private
45
+
46
+ def initialize_directories(root)
47
+ @doc_root_dir = File.join(root, API_DOCS_DIRNAME)
48
+
49
+ # remove previous data (and reset the directory)
50
+ FileUtils.rm_rf @doc_root_dir if File.exists?(@doc_root_dir)
51
+ FileUtils.mkdir_p @doc_root_dir unless File.exists? @doc_root_dir
52
+ end
53
+
54
+ def collect_resources
55
+ # load all resource definitions registered with Praxis
56
+ Praxis::Application.instance.resource_definitions.map do |resource|
57
+ # skip resources with doc_visibility of :none
58
+ next if resource.metadata[:doc_visibility] == :none
59
+ version = resource.version
60
+ # TODO: it seems that we shouldn't hardcode n/a in Praxis
61
+ # version = "unversioned" if version == "n/a"
62
+ @resources_by_version[version] << resource
63
+ end
64
+ end
65
+
66
+ def collect_types
67
+ @types_by_id = ObjectSpace.each_object( Class ).select do |obj|
68
+ obj < Attributor::Type
69
+ end.index_by(&:id)
70
+ end
71
+
72
+ def collect_infos
73
+ # All infos. Including keys for `:global`, "n/a", and any string version
74
+ @infos_by_version = ApiDefinition.instance.describe
75
+ end
76
+
77
+
78
+ # Recursively inspect the structure in data and collect any
79
+ # newly discovered types into the `reachable_types` in/out parameter
80
+ def collect_reachable_types( data, reachable_types )
81
+ case data
82
+ when Array
83
+ data.collect{|item| collect_reachable_types( item , reachable_types) }
84
+ when Hash
85
+ if data.key?(:type) && data[:type].kind_of?(Hash) && ( [:id,:name,:family] - data[:type].keys ).empty?
86
+ type_id = data[:type][:id]
87
+ unless type_id.nil?
88
+ unless types_by_id[type_id]
89
+ raise "Error! We have detected a reference to a 'Type' which is not derived from Attributor::Type" +
90
+ " Document generation cannot proceed."
91
+ end
92
+ reachable_types << types_by_id[type_id]
93
+ end
94
+ end
95
+ data.values.map{|item| collect_reachable_types( item , reachable_types)}
96
+ end
97
+ reachable_types
98
+ end
99
+
100
+ def write_index_file
101
+ # Gather the versions
102
+ versions = infos_by_version.keys.reject{|v| v == :global || v == :traits }.map do |version|
103
+ version == "n/a" ? "unversioned" : version
104
+ end
105
+ data = {
106
+ info: infos_by_version[:global][:info],
107
+ versions: versions
108
+ # Note, I don't think we need to report the global traits (but rather the ones in the version)
109
+ }
110
+ filename = File.join(doc_root_dir, "index-new.json")
111
+ puts "Generating Index file: #{filename}"
112
+ File.open(filename, 'w') {|f| f.write(JSON.pretty_generate(data))}
113
+ end
114
+
115
+ def write_version_file( version )
116
+ version_info = infos_by_version[version]
117
+ # Hack, let's "inherit/copy" all traits of a version from the global definition
118
+ # Eventually traits should be defined for a version (and inheritable from global) so we'll emulate that here
119
+ version_info[:traits] = infos_by_version[:traits]
120
+ dumped_resources = dump_resources( resources_by_version[version] )
121
+ found_media_types = resources_by_version[version].collect {|r| r.media_type.describe }
122
+
123
+ collected_types = Set.new
124
+ collect_reachable_types( dumped_resources, collected_types );
125
+ collect_reachable_types( found_media_types , collected_types );
126
+
127
+ dumped_schemas = dump_schemas( collected_types )
128
+
129
+ full_data = {
130
+ info: version_info[:info],
131
+ resources: dumped_resources,
132
+ schemas: nil,#dumped_schemas,
133
+ traits: version_info[:traits] || []
134
+ }
135
+ # Write the file
136
+ version_file = ( version == "n/a" ? "unversioned" : version )
137
+ filename = File.join(doc_root_dir, version_file)
138
+
139
+ puts "Generating API file: #{filename} (in json and yaml)"
140
+ File.open(filename+".json", 'w') {|f| f.write(JSON.pretty_generate(full_data))}
141
+ File.open(filename+".yml", 'w') {|f| f.write(YAML.dump(full_data))}
142
+ end
143
+
144
+
145
+ def dump_resources( resources )
146
+ resources.each_with_object({}) do |r, hash|
147
+ # Do not report undocumentable resources
148
+ next if r.metadata[:doc_visibility] == :none
149
+ context = [r.id]
150
+ resource_description = r.describe(context: context)
151
+
152
+ # strip actions with doc_visibility of :none
153
+ resource_description[:actions].reject! { |a| a[:metadata][:doc_visibility] == :none }
154
+
155
+ # Go through the params/payload of each action and augment them by
156
+ # adding a generated example (then stick it into the description hash)
157
+ r.actions.each do |action_name, action|
158
+ # skip actions with doc_visibility of :none
159
+ next if action.metadata[:doc_visibility] == :none
160
+
161
+ action_description = resource_description[:actions].find {|a| a[:name] == action_name }
162
+ end
163
+
164
+ hash[r.id] = resource_description
165
+ end
166
+ end
167
+
168
+
169
+ def dump_schemas(types)
170
+ reportable_types = types - EXCLUDED_TYPES_FROM_OUTPUT
171
+ reportable_types.each_with_object({}) do |type, array|
172
+ context = [type.id]
173
+ example_data = type.example(context)
174
+ type_output = type.describe(false, example: example_data)
175
+ unless type_output[:display_name]
176
+ # For non MediaTypes or pure types or anonymous types fallback to their name, and worst case to their id
177
+ type_output[:display_name] = type_output[:name] || type_output[:id]
178
+ end
179
+ if type_output[:views]
180
+ type_output[:views].delete(:master)
181
+ type_output[:views].each do |view_name, view_info|
182
+ view_info[:example] = example_data.render(view: view_name)
183
+ end
184
+ end
185
+ type_output[:example] = if example_data.respond_to? :render
186
+ example_data.render(view: :master)
187
+ else
188
+ type.dump(example_data)
189
+ end
190
+ array[type.id] = type_output
191
+ end
192
+ end
193
+
194
+
195
+ def dump_example_for(context_name, object)
196
+ example = object.example(Array(context_name))
197
+ if object.is_a? Praxis::Blueprint
198
+ example.render(view: :master)
199
+ elsif object.is_a? Attributor::Attribute
200
+ object.dump(example)
201
+ else
202
+ raise "Do not know how to dump this object (it is not a Blueprint or an Attribute): #{object}"
203
+ end
204
+ end
205
+
206
+ end
207
+ end
208
+ end
@@ -13,7 +13,8 @@ module Praxis
13
13
  # Generate a URL-encoded WWW form from structured data. Not implemented since this format
14
14
  # is not very useful for a response body.
15
15
  def generate(structured_data)
16
- raise NotImplementedError
16
+ return nil if structured_data.nil?
17
+ URI.encode_www_form(structured_data)
17
18
  end
18
19
  end
19
20
  end
@@ -80,7 +80,7 @@ module Praxis
80
80
  RUBY
81
81
  end
82
82
  end
83
-
83
+
84
84
  end
85
85
 
86
86
  end