praxis 0.17.1 → 0.18.0

Sign up to get free protection for your applications and to get access to all the features.
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