praxis 0.10.1 → 0.11pre

Sign up to get free protection for your applications and to get access to all the features.
Files changed (100) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +0 -1
  3. data/CHANGELOG.md +47 -10
  4. data/Gemfile +1 -1
  5. data/Guardfile +1 -0
  6. data/bin/praxis +33 -4
  7. data/lib/api_browser/app/css/main.css +0 -3
  8. data/lib/praxis.rb +16 -0
  9. data/lib/praxis/action_definition.rb +16 -18
  10. data/lib/praxis/application.rb +31 -2
  11. data/lib/praxis/bootloader.rb +37 -4
  12. data/lib/praxis/bootloader_stages/environment.rb +3 -7
  13. data/lib/praxis/bootloader_stages/plugin_config_load.rb +20 -0
  14. data/lib/praxis/bootloader_stages/plugin_config_prepare.rb +18 -0
  15. data/lib/praxis/bootloader_stages/plugin_loader.rb +19 -0
  16. data/lib/praxis/bootloader_stages/plugin_setup.rb +13 -0
  17. data/lib/praxis/bootloader_stages/routing.rb +16 -6
  18. data/lib/praxis/callbacks.rb +0 -2
  19. data/lib/praxis/config.rb +3 -2
  20. data/lib/praxis/dispatcher.rb +25 -13
  21. data/lib/praxis/error_handler.rb +16 -0
  22. data/lib/praxis/links.rb +9 -4
  23. data/lib/praxis/media_type_collection.rb +2 -3
  24. data/lib/praxis/notifications.rb +41 -0
  25. data/lib/praxis/plugin.rb +18 -8
  26. data/lib/praxis/plugin_concern.rb +40 -0
  27. data/lib/praxis/request.rb +27 -7
  28. data/lib/praxis/request_stages/action.rb +7 -2
  29. data/lib/praxis/request_stages/response.rb +7 -3
  30. data/lib/praxis/request_stages/validate_payload.rb +7 -1
  31. data/lib/praxis/resource_definition.rb +37 -16
  32. data/lib/praxis/response.rb +1 -0
  33. data/lib/praxis/responses/internal_server_error.rb +13 -8
  34. data/lib/praxis/responses/validation_error.rb +10 -7
  35. data/lib/praxis/restful_doc_generator.rb +312 -0
  36. data/lib/praxis/router.rb +7 -5
  37. data/lib/praxis/skeletor/restful_routing_config.rb +12 -5
  38. data/lib/praxis/stage.rb +5 -1
  39. data/lib/praxis/stats.rb +106 -0
  40. data/lib/praxis/tasks/api_docs.rb +8 -314
  41. data/lib/praxis/version.rb +1 -1
  42. data/praxis.gemspec +4 -1
  43. data/spec/functional_spec.rb +87 -32
  44. data/spec/praxis/action_definition_spec.rb +13 -12
  45. data/spec/praxis/bootloader_spec.rb +12 -5
  46. data/spec/praxis/notifications_spec.rb +23 -0
  47. data/spec/praxis/plugin_concern_spec.rb +21 -0
  48. data/spec/praxis/request_spec.rb +56 -1
  49. data/spec/praxis/request_stages_validate_spec.rb +3 -3
  50. data/spec/praxis/resource_definition_spec.rb +44 -60
  51. data/spec/praxis/responses/internal_server_error_spec.rb +32 -16
  52. data/spec/praxis/restful_routing_config_spec.rb +15 -2
  53. data/spec/praxis/router_spec.rb +5 -3
  54. data/spec/praxis/stats_spec.rb +9 -0
  55. data/spec/praxis_mapper_plugin_spec.rb +71 -0
  56. data/spec/spec_app/app/controllers/instances.rb +12 -0
  57. data/spec/spec_app/app/controllers/volumes.rb +5 -0
  58. data/spec/spec_app/app/models/person.rb +3 -0
  59. data/spec/spec_app/config/active_record.yml +2 -0
  60. data/spec/spec_app/config/authentication.yml +3 -0
  61. data/spec/spec_app/config/authorization.yml +4 -0
  62. data/spec/spec_app/config/environment.rb +28 -1
  63. data/spec/spec_app/config/praxis_mapper.yml +6 -0
  64. data/spec/spec_app/config/sequel_model.yml +2 -0
  65. data/spec/spec_app/config/stats.yml +8 -0
  66. data/spec/spec_app/config/stats.yml.dis +8 -0
  67. data/spec/spec_app/design/resources/instances.rb +53 -16
  68. data/spec/spec_app/design/resources/volumes.rb +13 -2
  69. data/spec/spec_helper.rb +14 -0
  70. data/spec/support/spec_authentication_plugin.rb +126 -0
  71. data/spec/support/spec_authorization_plugin.rb +95 -0
  72. data/spec/support/spec_praxis_mapper_plugin.rb +157 -0
  73. data/tasks/loader.thor +6 -0
  74. data/tasks/thor/app.rb +48 -0
  75. data/tasks/thor/example.rb +283 -0
  76. data/tasks/thor/templates/generator/empty_app/.gitignore +3 -0
  77. data/tasks/thor/templates/generator/empty_app/.rspec +1 -0
  78. data/tasks/thor/templates/generator/empty_app/Gemfile +29 -0
  79. data/tasks/thor/templates/generator/empty_app/Guardfile +3 -0
  80. data/tasks/thor/templates/generator/empty_app/README.md +4 -0
  81. data/tasks/thor/templates/generator/empty_app/Rakefile +25 -0
  82. data/tasks/thor/templates/generator/empty_app/app/models/.empty_directory +0 -0
  83. data/tasks/thor/templates/generator/empty_app/app/models/.gitkeep +0 -0
  84. data/tasks/thor/templates/generator/empty_app/app/responses/.empty_directory +0 -0
  85. data/tasks/thor/templates/generator/empty_app/app/responses/.gitkeep +0 -0
  86. data/tasks/thor/templates/generator/empty_app/app/v1/controllers/.empty_directory +0 -0
  87. data/tasks/thor/templates/generator/empty_app/app/v1/controllers/.gitkeep +0 -0
  88. data/tasks/thor/templates/generator/empty_app/config.ru +7 -0
  89. data/tasks/thor/templates/generator/empty_app/config/environment.rb +17 -0
  90. data/tasks/thor/templates/generator/empty_app/config/rainbows.rb +57 -0
  91. data/tasks/thor/templates/generator/empty_app/design/api.rb +0 -0
  92. data/tasks/thor/templates/generator/empty_app/design/response_templates/.empty_directory +0 -0
  93. data/tasks/thor/templates/generator/empty_app/design/response_templates/.gitkeep +0 -0
  94. data/tasks/thor/templates/generator/empty_app/design/v1/media_types/.empty_directory +0 -0
  95. data/tasks/thor/templates/generator/empty_app/design/v1/media_types/.gitkeep +0 -0
  96. data/tasks/thor/templates/generator/empty_app/design/v1/resources/.empty_directory +0 -0
  97. data/tasks/thor/templates/generator/empty_app/design/v1/resources/.gitkeep +0 -0
  98. data/tasks/thor/templates/generator/empty_app/spec/spec_helper.rb +18 -0
  99. metadata +97 -6
  100. data/tasks/praxis_app_generator.thor +0 -307
@@ -4,7 +4,12 @@ module Praxis
4
4
  class Action < RequestStage
5
5
 
6
6
  def execute
7
- response = controller.send(action.name, **request.params_hash)
7
+ if controller.method(action.name).arity == 0
8
+ response = controller.__send__(action.name)
9
+ else
10
+ response = controller.__send__(action.name, **request.params_hash)
11
+ end
12
+
8
13
  case response
9
14
  when String
10
15
  controller.response.body = response
@@ -12,7 +17,7 @@ module Praxis
12
17
  controller.response = response
13
18
  else
14
19
  raise "Action #{action.name} in #{controller.class} returned #{response.inspect}. Only Response objects or Strings allowed."
15
- end
20
+ end
16
21
  controller.response.request = request
17
22
  nil # Action cannot return its OK request, as it would indicate the end of the stage chain
18
23
  end
@@ -2,11 +2,13 @@ module Praxis
2
2
  module RequestStages
3
3
 
4
4
  class Response < RequestStage
5
+ WHITELIST_RESPONSES = [:validation_error]
5
6
 
6
7
  def execute
7
8
  response = controller.response
8
9
 
9
- unless action.responses.include?(response.response_name)
10
+
11
+ unless action.responses.include?(response.response_name) || WHITELIST_RESPONSES.include?(response.response_name)
10
12
  raise Exceptions::InvalidResponse.new(
11
13
  "Response #{response.name.inspect} is not allowed for #{action.name.inspect}"
12
14
  )
@@ -14,10 +16,12 @@ module Praxis
14
16
 
15
17
  response.handle
16
18
 
17
- praxis_config = Application.instance.config.praxis
18
- unless praxis_config && praxis_config.validate_responses == false
19
+ if Application.instance.config.praxis.validate_responses == true
19
20
  response.validate(action)
20
21
  end
22
+ rescue Exceptions::Validation => e
23
+ controller.response = Responses::ValidationError.new(exception: e)
24
+ retry
21
25
  end
22
26
 
23
27
  end
@@ -16,7 +16,13 @@ module Praxis
16
16
 
17
17
  def execute
18
18
  if request.action.payload
19
- request.load_payload(CONTEXT_FOR[:payload])
19
+ begin
20
+ request.load_payload(CONTEXT_FOR[:payload])
21
+ rescue Attributor::AttributorException => e
22
+ message = e.message
23
+ message << ". For request Content-Type: '#{request.content_type}'"
24
+ return Responses::ValidationError.new(exception: e, message: message)
25
+ end
20
26
  Attributor::AttributeResolver.current.register("payload",request.payload)
21
27
 
22
28
  errors = request.validate_payload(CONTEXT_FOR[:payload])
@@ -2,7 +2,6 @@ require 'active_support/concern'
2
2
  require 'active_support/inflector'
3
3
 
4
4
 
5
-
6
5
  module Praxis
7
6
  module ResourceDefinition
8
7
  extend ActiveSupport::Concern
@@ -11,6 +10,8 @@ module Praxis
11
10
  @version = 'n/a'.freeze
12
11
  @actions = Hash.new
13
12
  @responses = Hash.new
13
+ @action_defaults = []
14
+ @version_options = {}
14
15
  Application.instance.resource_definitions << self
15
16
  end
16
17
 
@@ -18,7 +19,8 @@ module Praxis
18
19
  attr_reader :actions
19
20
  attr_reader :routing_config
20
21
  attr_reader :responses
21
-
22
+ attr_reader :version_options
23
+
22
24
  attr_accessor :controller
23
25
 
24
26
  # FIXME: this is inconsistent with the rest of the magic DSL convention.
@@ -35,28 +37,49 @@ module Praxis
35
37
  @media_type = media_type
36
38
  end
37
39
 
38
- def version(version=nil)
40
+ def version(version=nil, options= { using: [:header,:params] }.freeze )
39
41
  return @version unless version
40
42
  @version = version
43
+ @version_options = options
41
44
  end
42
45
 
43
- def action(name, &block)
44
- @actions[name] = ActionDefinition.new(name, self, &block)
45
- end
46
+ def action_defaults(&block)
47
+ return @action_defaults unless block_given?
46
48
 
47
- def params(type=Attributor::Struct, **opts, &block)
48
- return @params if type == Attributor::Struct && !block
49
- @params = [type, opts, block]
49
+ @action_defaults << block
50
50
  end
51
+
52
+ def params(type=Attributor::Struct, **opts, &block)
53
+ warn 'DEPRECATION: ResourceDefinition.params is deprecated. Use it in action_defaults instead.'
54
+ action_defaults do
55
+ params type, **opts, &block
56
+ end
57
+ end
51
58
 
52
59
  def payload(type=Attributor::Struct, **opts, &block)
53
- return @payload if type == Attributor::Struct && !block
54
- @payload = [type, opts, block]
60
+ warn 'DEPRECATION: ResourceDefinition.payload is deprecated. Use action_defaults instead.'
61
+ action_defaults do
62
+ payload type, **opts, &block
63
+ end
55
64
  end
56
65
 
57
66
  def headers(**opts, &block)
58
- return @headers unless block
59
- @headers = [opts, block]
67
+ warn 'DEPRECATION: ResourceDefinition.headers is deprecated. Use action_defaults instead.'
68
+ action_defaults do
69
+ headers **opts, &block
70
+ end
71
+ end
72
+
73
+ def response(name, **args)
74
+ warn 'DEPRECATION: ResourceDefinition.response is deprecated. Use action_defaults instead.'
75
+ action_defaults do
76
+ response name, **args
77
+ end
78
+ end
79
+
80
+ def action(name, &block)
81
+ raise ArgumentError, "can not create ActionDefinition without block" unless block_given?
82
+ @actions[name] = ActionDefinition.new(name, self, &block)
60
83
  end
61
84
 
62
85
  def description(text=nil)
@@ -64,9 +87,7 @@ module Praxis
64
87
  @description
65
88
  end
66
89
 
67
- def response(name, **args)
68
- @responses[name] = args
69
- end
90
+
70
91
 
71
92
  def describe
72
93
  {}.tap do |hash|
@@ -94,6 +94,7 @@ module Praxis
94
94
  # @param [Object] action
95
95
  #
96
96
  def validate(action)
97
+ return if response_name == :validation_error
97
98
  unless ( response_definition = action.responses[response_name] )
98
99
  raise ArgumentError, "Attempting to return a response with name #{response_name} " \
99
100
  "but no response definition with that name can be found"
@@ -13,14 +13,20 @@ module Praxis
13
13
  @error = error
14
14
  end
15
15
 
16
- def format!(exception = @error) #_exception(exception)
16
+ def format!(exception = @error)
17
17
  if @error
18
- msg = {
19
- name: exception.class.name,
20
- message: exception.message,
21
- backtrace: exception.backtrace
22
- }
23
- msg[:cause] = format!(exception.cause) if exception.cause
18
+
19
+ if Application.instance.config.praxis.show_exceptions == true
20
+ msg = {
21
+ name: exception.class.name,
22
+ message: exception.message,
23
+ backtrace: exception.backtrace
24
+ }
25
+ msg[:cause] = format!(exception.cause) if exception.cause
26
+ else
27
+ msg = {name: 'InternalServerError', message: "Something bad happened."}
28
+ end
29
+
24
30
  @body = msg
25
31
  end
26
32
  end
@@ -37,4 +43,3 @@ module Praxis
37
43
  end
38
44
 
39
45
  end
40
-
@@ -3,23 +3,26 @@ module Praxis
3
3
  module Responses
4
4
 
5
5
  class ValidationError < BadRequest
6
- def initialize(errors: nil, exception: nil, **opts)
6
+ def initialize(errors: nil, exception: nil, message: nil, **opts)
7
7
  super(**opts)
8
8
  @headers['Content-Type'] = 'application/json' #TODO: might want an error mediatype
9
9
  @errors = errors
10
10
  @exception = exception
11
+ @message = message || (exception && exception.message)
11
12
  end
12
13
 
13
14
  def format!
14
15
  if @errors
15
16
  @body = {name: 'ValidationError', errors: @errors}
16
- elsif @exception
17
- @body = {name: 'ValidationError', message: @exception.message}
18
- if @exception.cause
19
- @body[:cause] = {name: @exception.cause.class.name, message: @exception.cause.message}
20
- end
21
- @body
17
+ elsif @message
18
+ @body = {name: 'ValidationError', message: @message}
22
19
  end
20
+
21
+ if @exception && @exception.cause
22
+ @body[:cause] = {name: @exception.cause.class.name, message: @exception.cause.message}
23
+ end
24
+
25
+ @body
23
26
  end
24
27
  end
25
28
 
@@ -0,0 +1,312 @@
1
+ module Praxis
2
+ class RestfulDocGenerator
3
+
4
+ class << self
5
+ attr_reader :inspected_types
6
+ end
7
+
8
+ @inspected_types = Set.new
9
+ API_DOCS_DIRNAME = 'api_docs'
10
+
11
+ EXCLUDED_TYPES_FROM_TOP_LEVEL = Set.new( [Attributor::Boolean, Attributor::CSV, Attributor::DateTime, Attributor::Float, Attributor::Hash, Attributor::Ids, Attributor::Integer, Attributor::Object, Attributor::String ] ).freeze
12
+
13
+ def self.inspect_attributes(the_type)
14
+
15
+ reachable = Set.new
16
+ return reachable if the_type.nil? || the_type.is_a?(Praxis::SimpleMediaType)
17
+
18
+ # If an attribute comes in, get its type
19
+ the_type = the_type.type if the_type.is_a? Attributor::Attribute
20
+
21
+ # Collection types are special since they wrap a member type, so let's reach in and grab it
22
+ the_type = the_type.member_attribute.type if the_type < Attributor::Collection
23
+
24
+ if @inspected_types.include? the_type
25
+ # We're done if we've already inspected it
26
+ return reachable
27
+ else
28
+ # Mark it as inspected (before recursing)
29
+ @inspected_types << the_type unless the_type.name == nil # Don't bother with anon structs
30
+ end
31
+ #puts "Inspecting type: #{the_type.name}" if the_type.name != nil
32
+
33
+ reachable << the_type unless the_type.name == nil # Don't bother with anon structs
34
+ if the_type.respond_to? :attributes
35
+ the_type.attributes.each do |name, attr|
36
+ attr_type = attr.type
37
+ #puts "Inspecting attr: #{name} (class: #{attr_type.name}) #{attr_type.inspect}"
38
+ reachable += self.inspect_attributes(attr_type)
39
+ end
40
+ end
41
+ reachable
42
+ end
43
+
44
+ class Resource
45
+
46
+ attr_accessor :media_type, :reachable_types, :version, :controller_config
47
+
48
+ def initialize( definition )
49
+ @controller_config = definition
50
+ if controller_config.version == 'n/a'
51
+ @version = 'unversioned'
52
+ else
53
+ @version = controller_config.version
54
+ end
55
+ @media_type = controller_config.media_type
56
+ @reachable_types = Set.new
57
+
58
+ # Collect reachable types from the media_type if any (plus itself)
59
+ if @media_type && ! @media_type.is_a?(Praxis::SimpleMediaType)
60
+ add_to_reachable RestfulDocGenerator.inspect_attributes(@media_type)
61
+ @media_type.attributes.each do |name, attr|
62
+ add_to_reachable RestfulDocGenerator.inspect_attributes(attr)
63
+ end
64
+ @generated_example = @media_type.example(self.id)
65
+ end
66
+
67
+ # Collect reachable types from the params and payload definitions
68
+ @controller_config.actions.each do |name, action_config|
69
+ add_to_reachable RestfulDocGenerator.inspect_attributes(action_config.params)
70
+ add_to_reachable RestfulDocGenerator.inspect_attributes(action_config.payload)
71
+ end
72
+
73
+ end
74
+
75
+ # TODO: I think that the "id"/"name" of a resource should be provided by the definition/controller...not derived here
76
+ def id
77
+ if @controller_config.controller
78
+ @controller_config.controller.name
79
+ else
80
+ # If an API doesn't quite have the controller defined, let's use the name from the resource definition
81
+ @controller_config.name
82
+ end
83
+ end
84
+
85
+ def add_to_reachable( found )
86
+ return if found == nil
87
+ @reachable_types += found
88
+ end
89
+ end
90
+
91
+ def initialize(root_dir)
92
+ @root_dir = root_dir
93
+ @doc_root_dir = File.join(@root_dir, API_DOCS_DIRNAME)
94
+ @resources = []
95
+
96
+ remove_previous_doc_data
97
+ load_resources
98
+
99
+ # Gather all reachable types (grouped by version)
100
+ types_for = Hash.new
101
+ @resources.each do |r|
102
+ types_for[r.version] ||= Set.new
103
+ types_for[r.version] += r.reachable_types
104
+ end
105
+
106
+ write_resources
107
+ write_types(types_for)
108
+ write_index(types_for)
109
+ write_templates(types_for)
110
+ end
111
+
112
+ def load_resources
113
+ Praxis::Application.instance.resource_definitions.map do |resource|
114
+ @resources << Resource.new(resource)
115
+ end
116
+
117
+ end
118
+
119
+ def dump_example_for(context_name, object)
120
+ example = object.example(Array(context_name))
121
+ if object.is_a? Praxis::Blueprint
122
+ example.render(:master)
123
+ elsif object.is_a? Attributor::Attribute
124
+ object.dump(example)
125
+ else
126
+ raise "Do not know how to dump this object (it is not a Blueprint or an Attribute): #{object}"
127
+ end
128
+ end
129
+
130
+ def write_resources
131
+ @resources.each do |r|
132
+ filename = File.join(@doc_root_dir, r.version, "resources","#{r.id}.json")
133
+ #puts "Dumping #{r.id} to #{filename}"
134
+ base = File.dirname(filename)
135
+ FileUtils.mkdir_p base unless File.exists? base
136
+ resource_description = r.controller_config.describe
137
+ # Go through the params/payload of each action and generate an example for them (then stick it into the description hash)
138
+ r.controller_config.actions.each do |action_name, action|
139
+ generated_examples = {}
140
+ if action.params
141
+ generated_examples[:params] = dump_example_for( r.id, action.params )
142
+ end
143
+ if action.payload
144
+ generated_examples[:payload] = dump_example_for( r.id, action.payload )
145
+ end
146
+ action_description = resource_description[:actions].find{|a| a[:name] == action_name }
147
+ action_description[:params][:example] = generated_examples[:params] if generated_examples[:params]
148
+ action_description[:payload][:example] = generated_examples[:payload] if generated_examples[:payload]
149
+ end
150
+
151
+ File.open(filename, 'w') {|f| f.write(JSON.pretty_generate(resource_description))}
152
+ end
153
+ end
154
+
155
+ def write_types( versioned_types )
156
+ versioned_types.each do |version, types|
157
+ dirname = File.join(@doc_root_dir, version, "types")
158
+ FileUtils.mkdir_p dirname unless File.exists? dirname
159
+ reportable_types = types - EXCLUDED_TYPES_FROM_TOP_LEVEL
160
+ reportable_types.each do |type|
161
+ filename = File.join(dirname, "#{type.name}.json")
162
+ #puts "Dumping #{type.name} to #{filename}"
163
+ type_output = type.describe
164
+ example_data = type.example(type.to_s)
165
+ if type_output[:views]
166
+ type_output[:views].delete(:master)
167
+ type_output[:views].each do |view_name, view_info|
168
+ # Add and example for each view
169
+ unless( type < Praxis::Links ) #TODO: why do we need to skip an example for links?
170
+ view_info[:example] = example_data.render(view_name)
171
+ end
172
+ end
173
+ end
174
+ # Save a full type example
175
+ # ...but not for links or link classes (there's no object container context if done alone!!)
176
+ unless( type < Praxis::Links ) #TODO: again, why is this special?
177
+ type_output[:example] = if example_data.respond_to? :render
178
+ example_data.render(:master)
179
+ else
180
+ example_data.dump
181
+ end
182
+ end
183
+
184
+ # add an example for each attribute??
185
+ File.open(filename, 'w') {|f| f.write(JSON.pretty_generate(type_output))}
186
+ end
187
+ end
188
+ end
189
+
190
+ # index looks like something like this:
191
+ # {'1.0':
192
+ # {
193
+ # // Typical entry for controller with an associated mediatype
194
+ # "Post" : { media_type: "V1::MT:Post" , controller: "V1:Ctrl:Post"}
195
+ # // Unusual entry for controller without an associated mediatype
196
+ # "Admin" : { controller: "V1:Ctrl:Admin" }
197
+ # // Entry for mediatype that somehow is not associated with any controller...
198
+ # "RemoteMT" : { media_type: "V1:Ctrl:RemoteMT" }
199
+ # // Entry to a non-primitive type (but not a mediatype), that it is not covered by any related controller or mt
200
+ # "Locale" : { kind: "Module::Locale"}
201
+ # }
202
+ #
203
+ # '2.0': { ... }
204
+ # }
205
+ def write_index( versioned_types )
206
+ index = Hash.new
207
+ media_types_seen_from_controllers = Set.new
208
+ # Process the resources first
209
+
210
+ @resources.each do |r|
211
+ index[r.version] ||= Hash.new
212
+ info = {controller: r.id}
213
+ if r.media_type
214
+ info[:media_type] = r.media_type.name
215
+ media_types_seen_from_controllers << r.media_type
216
+ end
217
+ display_name = r.id.split("::").last
218
+ index[r.version][display_name] = info
219
+ end
220
+
221
+ versioned_types.each do |version, types|
222
+ # Discard any mediatypes that we've already seen and processed as controller related
223
+ reportable_types = types - media_types_seen_from_controllers - EXCLUDED_TYPES_FROM_TOP_LEVEL
224
+ #TODO: think about these special cases, is it needed?
225
+ reportable_types.reject!{|type| type < Praxis::Links || type < Praxis::MediaTypeCollection }
226
+
227
+ reportable_types.each do |type|
228
+ index[version] ||= Hash.new
229
+ display_name = type.name.split("::").last + " (*)" #somehow this is just a MT so we probably wanna mark it different
230
+ if index[version].has_key? display_name
231
+ raise "Display name already taken for version #{version}! #{display_name}"
232
+ end
233
+ index[version][display_name] = if type < Praxis::MediaType
234
+ {media_type: type.name }
235
+ else
236
+ {kind: type.name}
237
+ end
238
+ end
239
+ end
240
+ filename = File.join(@doc_root_dir, "index.json")
241
+ dirname = File.dirname(filename)
242
+ FileUtils.mkdir_p dirname unless File.exists? dirname
243
+ File.open(filename, 'w') {|f| f.write(JSON.pretty_generate(index))}
244
+ end
245
+
246
+ def write_templates(versioned_types)
247
+ # Calculate and write top-level (non-versioned) templates
248
+ top_templates = write_template("")
249
+ # Calculate and write versioned templates (passing the top level ones for inheritance)
250
+ versioned_types.keys.each do |version|
251
+ write_template(version,top_templates)
252
+ end
253
+ end
254
+
255
+ def write_template(version,top_templates=nil)
256
+ # Collect template filenames (grouped by type: embedded vs. standalone)
257
+ templates_dir = File.join(@root_dir,"doc_browser","templates",version)
258
+
259
+ # Slurp in any top level (unversioned) templates if any
260
+ # Top level templates will apply to any versioned one (and can be overwritten
261
+ # if the version defines their own)
262
+ templates = {embedded: {}, standalone: {} }
263
+ if top_templates
264
+ templates[:embedded] = top_templates[:embedded].clone
265
+ templates[:standalone] = top_templates[:standalone].clone
266
+ end
267
+
268
+ dual = Dir.glob(File.join(templates_dir,"*.tmpl"))
269
+ embedded = Dir.glob(File.join(templates_dir,"embedded","*.tmpl"))
270
+ standalone = Dir.glob(File.join(templates_dir,"standalone","*.tmpl"))
271
+
272
+ # TODO: Encode the contents more appropriately rather than dumping a string
273
+ # Templates defined at the top will apply to both embedded and standalone
274
+ # But it can be overriden if the same type exists in the more specific directory
275
+ dual.each do |filename|
276
+ type_key = File.basename(filename).gsub(/.tmpl$/,'')
277
+ contents = IO.read(filename)
278
+ templates[:embedded][type_key] = contents
279
+ templates[:standalone][type_key] = contents
280
+ end
281
+
282
+ # For each embedded one, create a key in the embedded section, and encode the file contents in the value
283
+ embedded.each do |filename|
284
+ type_key = File.basename(filename).gsub(/.tmpl$/,'')
285
+ templates[:embedded][type_key] = IO.read(filename)
286
+ end
287
+ # For each standalone one, create a key in the standalone section, and encode the file contents in the value
288
+ standalone.each do |filename|
289
+ type_key = File.basename(filename).gsub(/.tmpl$/,'')
290
+ templates[:standalone][type_key] = IO.read(filename)
291
+ end
292
+
293
+ [:embedded,:standalone].each do |type|
294
+ v = version.empty? ? "top level": version
295
+ puts "Packaging #{v} #{type} templates for: #{templates[type].keys}" unless templates[type].keys.empty?
296
+ end
297
+
298
+ # Write the resulting hash to the final file in the docs directory
299
+ filename = File.join(@doc_root_dir, version, "templates.json")
300
+ dirname = File.dirname(filename)
301
+ FileUtils.mkdir_p dirname unless File.exists? dirname
302
+ File.open(filename, 'w') {|f| f.write(JSON.pretty_generate(templates))}
303
+ return templates
304
+ end
305
+ private
306
+
307
+ def remove_previous_doc_data
308
+ FileUtils.rm_rf @doc_root_dir if File.exists?(@doc_root_dir)
309
+ end
310
+
311
+ end
312
+ end