praxis 2.0.pre.4 → 2.0.pre.5
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 +4 -4
- data/CHANGELOG.md +5 -1
- data/lib/praxis.rb +1 -0
- data/lib/praxis/api_general_info.rb +21 -0
- data/lib/praxis/docs/generator.rb +11 -6
- data/lib/praxis/docs/open_api_generator.rb +255 -0
- data/lib/praxis/docs/openapi/info_object.rb +31 -0
- data/lib/praxis/docs/openapi/media_type_object.rb +59 -0
- data/lib/praxis/docs/openapi/operation_object.rb +40 -0
- data/lib/praxis/docs/openapi/parameter_object.rb +69 -0
- data/lib/praxis/docs/openapi/paths_object.rb +58 -0
- data/lib/praxis/docs/openapi/request_body_object.rb +51 -0
- data/lib/praxis/docs/openapi/response_object.rb +63 -0
- data/lib/praxis/docs/openapi/responses_object.rb +44 -0
- data/lib/praxis/docs/openapi/schema_object.rb +87 -0
- data/lib/praxis/docs/openapi/server_object.rb +24 -0
- data/lib/praxis/docs/openapi/tag_object.rb +21 -0
- data/lib/praxis/extensions/attribute_filtering/filtering_params.rb +4 -0
- data/lib/praxis/extensions/field_selection/field_selector.rb +4 -0
- data/lib/praxis/links.rb +4 -0
- data/lib/praxis/mapper/selector_generator.rb +0 -4
- data/lib/praxis/multipart/part.rb +5 -2
- data/lib/praxis/response_definition.rb +1 -1
- data/lib/praxis/tasks/api_docs.rb +23 -0
- data/lib/praxis/types/media_type_common.rb +10 -0
- data/lib/praxis/types/multipart_array.rb +62 -0
- data/lib/praxis/version.rb +1 -1
- data/praxis.gemspec +2 -2
- data/spec/praxis/response_definition_spec.rb +7 -4
- data/spec/spec_app/design/api.rb +6 -0
- metadata +18 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 05fc37d45d8686251eb9691f05b7c9cdcfb3a8fe
|
4
|
+
data.tar.gz: fd5b9c01c4eab9994319eedf5cae50957b8dce06
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d8c4a7698ce165605a1c3a59c7fd014490b86a2bd4a27b2814e05dd1e9fa87d268a88dbd549239a07707f18b9c0a2fa350f45725686a50ef0777e1a32a11a7dc
|
7
|
+
data.tar.gz: 1a93f51757421d516ee80eb0d38101c6f0eff7d9cd9483ebb4fb44634342edf88e5ff7b16b49676cb696e7653843c1a69b7d505eaa4bde954e0bb7ae60b7f13e
|
data/CHANGELOG.md
CHANGED
@@ -1,6 +1,10 @@
|
|
1
1
|
# Praxis Changelog
|
2
2
|
|
3
|
-
##
|
3
|
+
## 2.0.pre.5
|
4
|
+
|
5
|
+
- Added support for OpenAPI 3.x document generation. Consider this in Beta state, although it is fairly close to feature complete.
|
6
|
+
|
7
|
+
## 2.0.pre.4
|
4
8
|
- Reworked the field selection DB query generation to support full tree of eager loaded dependencies
|
5
9
|
- Built support for both ActiveRecord and Sequel gems
|
6
10
|
- Selected DB fields will include/map the defined resource properties and will always include any necessary fields on both sides of the joins for the given associations.
|
data/lib/praxis.rb
CHANGED
@@ -15,6 +15,19 @@ module Praxis
|
|
15
15
|
end
|
16
16
|
end
|
17
17
|
|
18
|
+
# Allow any custom method to get/set any value
|
19
|
+
def method_missing(name, val=nil)
|
20
|
+
if val.nil?
|
21
|
+
get(name)
|
22
|
+
else
|
23
|
+
set(name, val)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def respond_to_missing?(*)
|
28
|
+
true
|
29
|
+
end
|
30
|
+
|
18
31
|
def get(k)
|
19
32
|
return @data[k] if @data.key?(k)
|
20
33
|
return @global_info.get(k) if @global_info
|
@@ -41,6 +54,14 @@ module Praxis
|
|
41
54
|
end
|
42
55
|
end
|
43
56
|
|
57
|
+
def logo_url(val=nil)
|
58
|
+
if val.nil?
|
59
|
+
get(:logo_url)
|
60
|
+
else
|
61
|
+
set(:logo_url, val)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
44
65
|
def description(val=nil)
|
45
66
|
if val.nil?
|
46
67
|
get(:description)
|
@@ -120,12 +120,7 @@ module Praxis
|
|
120
120
|
File.open(filename, 'w') {|f| f.write(JSON.pretty_generate(data))}
|
121
121
|
end
|
122
122
|
|
123
|
-
def
|
124
|
-
version_info = infos_by_version[version]
|
125
|
-
# Hack, let's "inherit/copy" all traits of a version from the global definition
|
126
|
-
# Eventually traits should be defined for a version (and inheritable from global) so we'll emulate that here
|
127
|
-
version_info[:traits] = infos_by_version[:traits]
|
128
|
-
dumped_resources = dump_resources( resources_by_version[version] )
|
123
|
+
def scan_types_for_version(version, dumped_resources)
|
129
124
|
found_media_types = resources_by_version[version].select{|r| r.media_type}.collect {|r| r.media_type.describe }
|
130
125
|
|
131
126
|
# We'll start by processing the rendered mediatypes
|
@@ -150,7 +145,17 @@ module Praxis
|
|
150
145
|
processed_types += newfound
|
151
146
|
newfound = scan_dump_for_types( dumped, processed_types )
|
152
147
|
end
|
148
|
+
processed_types
|
149
|
+
end
|
153
150
|
|
151
|
+
def write_version_file( version )
|
152
|
+
version_info = infos_by_version[version]
|
153
|
+
# Hack, let's "inherit/copy" all traits of a version from the global definition
|
154
|
+
# Eventually traits should be defined for a version (and inheritable from global) so we'll emulate that here
|
155
|
+
version_info[:traits] = infos_by_version[:traits]
|
156
|
+
|
157
|
+
dumped_resources = dump_resources( resources_by_version[version] )
|
158
|
+
processed_types = scan_types_for_version(version, dumped_resources)
|
154
159
|
dumped_schemas = dump_schemas( processed_types )
|
155
160
|
full_data = {
|
156
161
|
info: version_info[:info],
|
@@ -0,0 +1,255 @@
|
|
1
|
+
require_relative 'openapi/info_object.rb'
|
2
|
+
require_relative 'openapi/server_object.rb'
|
3
|
+
require_relative 'openapi/paths_object.rb'
|
4
|
+
require_relative 'openapi/tag_object.rb'
|
5
|
+
|
6
|
+
module Praxis
|
7
|
+
module Docs
|
8
|
+
|
9
|
+
class OpenApiGenerator < Generator
|
10
|
+
API_DOCS_DIRNAME = 'docs/openapi'
|
11
|
+
|
12
|
+
# substitutes ":params_like_so" for {params_like_so}
|
13
|
+
def self.templatize_url( string )
|
14
|
+
Mustermann.new(string).to_templates.first
|
15
|
+
end
|
16
|
+
|
17
|
+
def save!
|
18
|
+
initialize_directories
|
19
|
+
# Restrict the versions listed in the index file to the ones for which we have at least 1 resource
|
20
|
+
write_index_file( for_versions: resources_by_version.keys )
|
21
|
+
resources_by_version.keys.each do |version|
|
22
|
+
write_version_file(version)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def initialize(root)
|
27
|
+
require 'yaml'
|
28
|
+
@root = root
|
29
|
+
@resources_by_version = Hash.new do |h,k|
|
30
|
+
h[k] = Set.new
|
31
|
+
end
|
32
|
+
|
33
|
+
@infos = ApiDefinition.instance.infos
|
34
|
+
collect_resources
|
35
|
+
collect_types
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def write_index_file( for_versions: )
|
41
|
+
# TODO. create a simple html file that can link to the individual versions available
|
42
|
+
end
|
43
|
+
|
44
|
+
def write_version_file( version )
|
45
|
+
# version_info = infos_by_version[version]
|
46
|
+
# # Hack, let's "inherit/copy" all traits of a version from the global definition
|
47
|
+
# # Eventually traits should be defined for a version (and inheritable from global) so we'll emulate that here
|
48
|
+
# version_info[:traits] = infos_by_version[:traits]
|
49
|
+
dumped_resources = dump_resources( resources_by_version[version] )
|
50
|
+
processed_types = scan_types_for_version(version, dumped_resources)
|
51
|
+
|
52
|
+
# Here we have:
|
53
|
+
# processed types: which includes mediatypes and normal types...real classes
|
54
|
+
# processed resources for this version: resources_by_version[version]
|
55
|
+
|
56
|
+
info_object = OpenApi::InfoObject.new(version: version, api_definition_info: @infos[version])
|
57
|
+
# We only support a server in Praxis ... so we'll use the base path
|
58
|
+
server_object = OpenApi::ServerObject.new( url: @infos[version].base_path )
|
59
|
+
|
60
|
+
paths_object = OpenApi::PathsObject.new( resources: resources_by_version[version])
|
61
|
+
|
62
|
+
full_data = {
|
63
|
+
openapi: "3.0.2",
|
64
|
+
info: info_object.dump,
|
65
|
+
servers: [server_object.dump],
|
66
|
+
paths: paths_object.dump,
|
67
|
+
# responses: {}, #TODO!! what do we get here? the templates?...need to transform to "Responses Definitions Object"
|
68
|
+
# securityDefinitions: {}, # NOTE: No security definitions in Praxis
|
69
|
+
# security: [], # NOTE: No security definitions in Praxis
|
70
|
+
}
|
71
|
+
|
72
|
+
# Create the top level tags by:
|
73
|
+
# 1- First adding all the resource display names (and descriptions)
|
74
|
+
tags_for_resources = resources_by_version[version].collect do |resource|
|
75
|
+
OpenApi::TagObject.new(name: resource.display_name, description: resource.description ).dump
|
76
|
+
end
|
77
|
+
full_data[:tags] = tags_for_resources
|
78
|
+
# 2- Then adding all of the top level traits but marking them special with the x-traitTag (of Redoc)
|
79
|
+
tags_for_traits = (ApiDefinition.instance.traits).collect do |name, info|
|
80
|
+
OpenApi::TagObject.new(name: name, description: info.description).dump.merge(:'x-traitTag' => true)
|
81
|
+
end
|
82
|
+
unless tags_for_traits.empty?
|
83
|
+
full_data[:tags] = full_data[:tags] + tags_for_traits
|
84
|
+
end
|
85
|
+
|
86
|
+
# Include only MTs (i.e., not custom types or simple types...)
|
87
|
+
component_schemas = reusable_schema_objects(processed_types.select{|t| t < Praxis::MediaType})
|
88
|
+
|
89
|
+
# 3- Then adding all of the top level Mediatypes...so we can present them at the bottom, otherwise they don't show
|
90
|
+
tags_for_mts = component_schemas.map do |(name, info)|
|
91
|
+
special_redoc_anchor = "<SchemaDefinition schemaRef=\"#/components/schemas/#{name}\" showReadOnly={true} showWriteOnly={true} />"
|
92
|
+
guessed_display = name.split('-').last # TODO!!!the informational hash does not seem to come with the "description" value set...hmm
|
93
|
+
OpenApi::TagObject.new(name: name, description: special_redoc_anchor).dump.merge(:'x-displayName' => guessed_display)
|
94
|
+
end
|
95
|
+
unless tags_for_mts.empty?
|
96
|
+
full_data[:tags] = full_data[:tags] + tags_for_mts
|
97
|
+
end
|
98
|
+
|
99
|
+
# Include all the reusable schemas in the components hash
|
100
|
+
full_data[:components] = {
|
101
|
+
schemas: component_schemas
|
102
|
+
}
|
103
|
+
|
104
|
+
# REDOC specific grouping of sidebar
|
105
|
+
resource_tags = { name: 'Resources', tags: tags_for_resources.map{|t| t[:name]} }
|
106
|
+
schema_tags = { name: 'Models', tags: tags_for_mts.map{|t| t[:name]} }
|
107
|
+
full_data['x-tagGroups'] = [resource_tags, schema_tags]
|
108
|
+
|
109
|
+
# if parameter_object = convert_to_parameter_object( version_info[:info][:base_params] )
|
110
|
+
# full_data[:parameters] = parameter_object
|
111
|
+
# end
|
112
|
+
#puts JSON.pretty_generate( full_data )
|
113
|
+
# Write the file
|
114
|
+
version_file = ( version == "n/a" ? "unversioned" : version )
|
115
|
+
filename = File.join(doc_root_dir, version_file, 'openapi')
|
116
|
+
|
117
|
+
puts "Generating Open API file : #{filename} (json and yml) "
|
118
|
+
json_data = JSON.pretty_generate(full_data)
|
119
|
+
File.open(filename+".json", 'w') {|f| f.write(json_data)}
|
120
|
+
converted_full_data = JSON.parse( json_data ) # So symbols disappear
|
121
|
+
File.open(filename+".yml", 'w') {|f| f.write(YAML.dump(converted_full_data))}
|
122
|
+
|
123
|
+
html =<<-EOB
|
124
|
+
<!DOCTYPE html>
|
125
|
+
<html>
|
126
|
+
<head>
|
127
|
+
<title>ReDoc</title>
|
128
|
+
<!-- needed for adaptive design -->
|
129
|
+
<meta charset="utf-8"/>
|
130
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
131
|
+
<link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet">
|
132
|
+
|
133
|
+
<!--
|
134
|
+
ReDoc doesn't change outer page styles
|
135
|
+
-->
|
136
|
+
<style>
|
137
|
+
body {
|
138
|
+
margin: 0;
|
139
|
+
padding: 0;
|
140
|
+
}
|
141
|
+
</style>
|
142
|
+
</head>
|
143
|
+
<body>
|
144
|
+
<redoc spec-url='http://localhost:9090/#{version_file}/openapi.json'></redoc>
|
145
|
+
<script src="https://cdn.jsdelivr.net/npm/redoc@next/bundles/redoc.standalone.js"> </script>
|
146
|
+
</body>
|
147
|
+
</html>
|
148
|
+
EOB
|
149
|
+
html_file = File.join(doc_root_dir, version_file, 'index.html')
|
150
|
+
File.write(html_file, html)
|
151
|
+
end
|
152
|
+
|
153
|
+
def initialize_directories
|
154
|
+
@doc_root_dir = File.join(@root, API_DOCS_DIRNAME)
|
155
|
+
|
156
|
+
# remove previous data (and reset the directory)
|
157
|
+
FileUtils.rm_rf @doc_root_dir if File.exists?(@doc_root_dir)
|
158
|
+
FileUtils.mkdir_p @doc_root_dir unless File.exists? @doc_root_dir
|
159
|
+
resources_by_version.keys.each do |version|
|
160
|
+
FileUtils.mkdir_p @doc_root_dir + '/' + version
|
161
|
+
end
|
162
|
+
FileUtils.mkdir_p @doc_root_dir + '/unversioned'
|
163
|
+
end
|
164
|
+
|
165
|
+
def normalize_media_types( mtis )
|
166
|
+
mtis.collect do |mti|
|
167
|
+
MediaTypeIdentifier.load(mti).to_s
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
def reusable_schema_objects(types)
|
172
|
+
types.each_with_object({}) do |(type), accum|
|
173
|
+
the_type = \
|
174
|
+
if type.respond_to? :as_json_schema
|
175
|
+
type
|
176
|
+
else # If it is a blueprint ... for now, it'd be through the attribute
|
177
|
+
type.attribute
|
178
|
+
end
|
179
|
+
accum[type.id] = the_type.as_json_schema(shallow: false)
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
def convert_to_parameter_object( params )
|
184
|
+
# TODO!! actually convert each of them
|
185
|
+
puts "TODO! convert to parameter object"
|
186
|
+
params
|
187
|
+
end
|
188
|
+
|
189
|
+
def convert_traits_to_tags( traits )
|
190
|
+
traits.collect do |name, info|
|
191
|
+
{ name: name, description: info[:description] }
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
|
196
|
+
def dump_responses_object( responses )
|
197
|
+
responses.each_with_object({}) do |(name, info), hash|
|
198
|
+
data = { description: info[:description] || "" }
|
199
|
+
if payload = info[:payload]
|
200
|
+
body_type= payload[:id]
|
201
|
+
raise "WAIT! response payload doesn't have an existing id for the schema!!! (do an if, and describe it if so)" unless body_type
|
202
|
+
data[:schema] = {"$ref" => "#/definitions/#{body_type}" }
|
203
|
+
end
|
204
|
+
|
205
|
+
# data[:schema] = ???TODO!!
|
206
|
+
if headers_object = dump_response_headers_object( info[:headers] )
|
207
|
+
data[:headers] = headers_object
|
208
|
+
end
|
209
|
+
if info[:payload] && ( examples_object = dump_response_examples_object( info[:payload][:examples] ) )
|
210
|
+
data[:examples] = examples_object
|
211
|
+
end
|
212
|
+
hash[info[:status]] = data
|
213
|
+
end
|
214
|
+
end
|
215
|
+
# def dump_response_headers_object( headers )
|
216
|
+
# puts "WARNING!! Finish this. It seems that headers for responses are never set in the hash??"
|
217
|
+
# unless headers.empty?
|
218
|
+
# binding.pry
|
219
|
+
# puts headers
|
220
|
+
# end
|
221
|
+
# end
|
222
|
+
|
223
|
+
def dump_response_examples_object( examples )
|
224
|
+
examples.each_with_object({}) do |(name, info), hash|
|
225
|
+
hash[info[:content_type]] = info[:body]
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
|
230
|
+
def dump_resources( resources )
|
231
|
+
resources.each_with_object({}) do |r, hash|
|
232
|
+
# Do not report undocumentable resources
|
233
|
+
next if r.metadata[:doc_visibility] == :none
|
234
|
+
context = [r.id]
|
235
|
+
resource_description = r.describe(context: context)
|
236
|
+
|
237
|
+
# strip actions with doc_visibility of :none
|
238
|
+
resource_description[:actions].reject! { |a| a[:metadata][:doc_visibility] == :none }
|
239
|
+
|
240
|
+
# Go through the params/payload of each action and augment them by
|
241
|
+
# adding a generated example (then stick it into the description hash)
|
242
|
+
r.actions.each do |action_name, action|
|
243
|
+
# skip actions with doc_visibility of :none
|
244
|
+
next if action.metadata[:doc_visibility] == :none
|
245
|
+
|
246
|
+
action_description = resource_description[:actions].find {|a| a[:name] == action_name }
|
247
|
+
end
|
248
|
+
|
249
|
+
hash[r.id] = resource_description
|
250
|
+
end
|
251
|
+
end
|
252
|
+
|
253
|
+
end
|
254
|
+
end
|
255
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Praxis
|
2
|
+
module Docs
|
3
|
+
module OpenApi
|
4
|
+
class InfoObject
|
5
|
+
attr_reader :info, :version
|
6
|
+
def initialize(version: , api_definition_info: )
|
7
|
+
@version = version
|
8
|
+
@info = api_definition_info
|
9
|
+
raise "OpenApi docs require a 'Title' for your API." unless info.title
|
10
|
+
end
|
11
|
+
|
12
|
+
def dump
|
13
|
+
data ={
|
14
|
+
title: info.title,
|
15
|
+
description: info.description,
|
16
|
+
termsOfService: info.termsOfService,
|
17
|
+
contact: info.contact,
|
18
|
+
license: info.license,
|
19
|
+
version: version,
|
20
|
+
:'x-name' => info.name,
|
21
|
+
:'x-logo' => {
|
22
|
+
url: info.logo_url,
|
23
|
+
backgroundColor: "#FFFFFF",
|
24
|
+
altText: info.title
|
25
|
+
}
|
26
|
+
}
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module Praxis
|
2
|
+
module Docs
|
3
|
+
module OpenApi
|
4
|
+
class MediaTypeObject
|
5
|
+
attr_reader :schema, :example
|
6
|
+
def initialize(schema:, example:)
|
7
|
+
@schema = schema
|
8
|
+
@example = example
|
9
|
+
end
|
10
|
+
|
11
|
+
def dump
|
12
|
+
{
|
13
|
+
schema: schema,
|
14
|
+
example: example,
|
15
|
+
# encoding: TODO SUPPORT IT maybe be great/necessary for multipart
|
16
|
+
}
|
17
|
+
end
|
18
|
+
|
19
|
+
# Helper to create the typical content attribute for responses and request bodies
|
20
|
+
def self.create_content_attribute_helper(type: , example_payload:, example_handlers: nil)
|
21
|
+
# Will produce 1 example encoded with a given handler (and marking it with the given content type)
|
22
|
+
unless example_handlers
|
23
|
+
example_handlers = [ {'application/json' => 'json' } ]
|
24
|
+
end
|
25
|
+
# NOTE2: we should just create a $ref here unless it's an anon mediatype...
|
26
|
+
return {} if type.is_a? SimpleMediaType # NOTE: skip if it's a SimpleMediaType?? ... is that correct?
|
27
|
+
|
28
|
+
the_schema = if type.anonymous? || ! (type < Praxis::MediaType) # Avoid referencing custom/simple Types? (i.e., just MTs)
|
29
|
+
SchemaObject.new(info: type).dump_schema
|
30
|
+
else
|
31
|
+
{ '$ref': "#/components/schemas/#{type.id}" }
|
32
|
+
end
|
33
|
+
|
34
|
+
if example_payload
|
35
|
+
examples_by_content_type = {}
|
36
|
+
rendered_payload = example_payload.dump
|
37
|
+
|
38
|
+
example_handlers.each do |spec|
|
39
|
+
content_type, handler_name = spec.first
|
40
|
+
handler = Praxis::Application.instance.handlers[handler_name]
|
41
|
+
# ReDoc is not happy to display json generated outputs when served as JSON...wtf?
|
42
|
+
generated = handler.generate(rendered_payload)
|
43
|
+
final = ( handler_name == 'json') ? JSON.parse(generated) : generated
|
44
|
+
examples_by_content_type[content_type] = final
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# Key string (of MT) , value MTObject
|
49
|
+
content_hash = examples_by_content_type.each_with_object({}) do |(content_type, example_hash),accum|
|
50
|
+
accum[content_type] = MediaTypeObject.new(
|
51
|
+
schema: the_schema, # Every MT will have the same exact type..oh well .. maybe a REF?
|
52
|
+
example: example_hash,
|
53
|
+
).dump
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require_relative 'parameter_object'
|
2
|
+
require_relative 'request_body_object'
|
3
|
+
require_relative 'responses_object'
|
4
|
+
|
5
|
+
module Praxis
|
6
|
+
module Docs
|
7
|
+
module OpenApi
|
8
|
+
class OperationObject
|
9
|
+
# https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#operation-object
|
10
|
+
attr_reader :id, :url, :action, :tags
|
11
|
+
def initialize(id:, url:, action:, tags:)
|
12
|
+
@id = id
|
13
|
+
@url = url
|
14
|
+
@action = action
|
15
|
+
@tags = tags
|
16
|
+
end
|
17
|
+
|
18
|
+
def dump
|
19
|
+
all_parameters = ParameterObject.process_parameters(action)
|
20
|
+
all_tags = tags + action.traits
|
21
|
+
h = {
|
22
|
+
summary: action.name.to_s,
|
23
|
+
description: action.description,
|
24
|
+
#externalDocs: {}, # TODO/FIXME
|
25
|
+
operationId: id,
|
26
|
+
responses: ResponsesObject.new(responses: action.responses).dump,
|
27
|
+
# callbacks
|
28
|
+
# deprecated: false
|
29
|
+
# security: [{}]
|
30
|
+
# servers: [{}]
|
31
|
+
}
|
32
|
+
h[:tags] = all_tags.uniq unless all_tags.empty?
|
33
|
+
h[:parameters] = all_parameters unless all_parameters.empty?
|
34
|
+
h[:requestBody] = RequestBodyObject.new(attribute: action.payload ).dump if action.payload
|
35
|
+
h
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
require_relative 'schema_object'
|
2
|
+
|
3
|
+
module Praxis
|
4
|
+
module Docs
|
5
|
+
module OpenApi
|
6
|
+
class ParameterObject
|
7
|
+
# https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#parameter-object
|
8
|
+
attr_reader :location, :name, :is_required, :info
|
9
|
+
def initialize(location: , name:, is_required:, info:)
|
10
|
+
@location = location
|
11
|
+
@name = name
|
12
|
+
@info = info
|
13
|
+
@is_required = is_required
|
14
|
+
end
|
15
|
+
|
16
|
+
def dump
|
17
|
+
# Fixed fields
|
18
|
+
h = { name: name, in: location }
|
19
|
+
h[:description] = info.options[:description] if info.options[:description]
|
20
|
+
h[:required] = is_required if is_required
|
21
|
+
# h[:deprecated] = false
|
22
|
+
# h[:allowEmptyValue] ??? TODO: support in Praxis
|
23
|
+
|
24
|
+
# Other supported attributes
|
25
|
+
# style
|
26
|
+
# explode
|
27
|
+
# allowReserved
|
28
|
+
|
29
|
+
# Now merge the rest schema and example
|
30
|
+
# schema
|
31
|
+
# example
|
32
|
+
# examples (Example and Examples are mutually exclusive)
|
33
|
+
schema = SchemaObject.new(info: info)
|
34
|
+
h[:schema] = schema.dump_schema
|
35
|
+
# Note: we do not support the 'content' key...we always use schema
|
36
|
+
h[:example] = schema.dump_example
|
37
|
+
h
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.process_parameters( action )
|
41
|
+
output = []
|
42
|
+
# An array, with one hash per param inside
|
43
|
+
if action.headers
|
44
|
+
(action.headers.attributes||{}).each_with_object(output) do |(name, info), out|
|
45
|
+
out << ParameterObject.new( location: 'header', name: name, is_required: info.options[:required], info: info ).dump
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
if action.params
|
50
|
+
route_params = \
|
51
|
+
if action.primary_route.nil?
|
52
|
+
warn "Warning: No routes defined for action #{action.name}"
|
53
|
+
[]
|
54
|
+
else
|
55
|
+
action.primary_route.path.named_captures.keys.collect(&:to_sym)
|
56
|
+
end
|
57
|
+
(action.params.attributes||{}).each_with_object(output) do |(name, info), out|
|
58
|
+
in_type = route_params.include?(name) ? :path : :query
|
59
|
+
is_required = (in_type == :path ) ? true : info.options[:required]
|
60
|
+
out << ParameterObject.new( location: in_type, name: name, is_required: is_required, info: info ).dump
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
output
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require_relative 'operation_object.rb'
|
2
|
+
module Praxis
|
3
|
+
module Docs
|
4
|
+
module OpenApi
|
5
|
+
class PathsObject
|
6
|
+
# https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#paths-object
|
7
|
+
attr_reader :resources, :paths
|
8
|
+
def initialize(resources:)
|
9
|
+
@resources = resources
|
10
|
+
# A hash with keys of paths, and values of hash
|
11
|
+
# where the subhash has verb keys and path_items as values
|
12
|
+
# {
|
13
|
+
# "/pets": {
|
14
|
+
# "get": {...},
|
15
|
+
# "post": { ...}
|
16
|
+
# "/humans": {
|
17
|
+
# "get": {...},
|
18
|
+
@paths = Hash.new {|h,k| h[k] = {} }
|
19
|
+
end
|
20
|
+
|
21
|
+
|
22
|
+
def dump
|
23
|
+
resources.each do |resource|
|
24
|
+
compute_resource_paths( resource )
|
25
|
+
end
|
26
|
+
paths
|
27
|
+
end
|
28
|
+
|
29
|
+
def compute_resource_paths( resource )
|
30
|
+
id = resource.id
|
31
|
+
# fill in the paths hash with a key for each path for each action/route
|
32
|
+
resource.actions.each do |action_name, action|
|
33
|
+
params_example = action.params ? action.params.example(nil) : nil
|
34
|
+
urls = action.routes.collect do |route|
|
35
|
+
ActionDefinition.url_description(route: route, params: action.params, params_example: params_example)
|
36
|
+
end.compact
|
37
|
+
urls.each do |url|
|
38
|
+
verb = url[:verb].downcase
|
39
|
+
templetized_path = OpenApiGenerator.templatize_url(url[:path])
|
40
|
+
path_entry = paths[templetized_path]
|
41
|
+
# Let's fill in verb stuff within the working hash
|
42
|
+
raise "VERB #{_verb} already defined for #{id}!?!?!" if path_entry[verb]
|
43
|
+
|
44
|
+
action_uid = "action-#{action_name}-#{id}"
|
45
|
+
# Add a tag matching the resource name (hoping all actions of a resource are grouped)
|
46
|
+
action_tags = [resource.display_name]
|
47
|
+
path_entry[verb] = OperationObject.new( id: action_uid, url: url, action: action, tags: action_tags).dump
|
48
|
+
end
|
49
|
+
end
|
50
|
+
# For each path, we can further annotate with
|
51
|
+
# servers
|
52
|
+
# parameters
|
53
|
+
# But we don't have that concept in praxis
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require_relative 'schema_object'
|
2
|
+
|
3
|
+
module Praxis
|
4
|
+
module Docs
|
5
|
+
module OpenApi
|
6
|
+
class RequestBodyObject
|
7
|
+
# https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#request-body-object
|
8
|
+
attr_reader :attribute
|
9
|
+
def initialize(attribute:)
|
10
|
+
@attribute = attribute
|
11
|
+
end
|
12
|
+
|
13
|
+
def dump
|
14
|
+
h = {}
|
15
|
+
h[:description] = attribute.options[:description] if attribute.options[:description]
|
16
|
+
h[:required] = attribute.options[:required] || false
|
17
|
+
|
18
|
+
# OpenApi wants a set of bodies per MediaType/Content-Type
|
19
|
+
# For us there's really only one schema (regardless of encoding)...
|
20
|
+
# so we'll show all the supported MTs...but repeating the schema
|
21
|
+
#dumped_schema = SchemaObject.new(info: attribute).dump_schema
|
22
|
+
|
23
|
+
example_handlers = if attribute.type < Praxis::Types::MultipartArray
|
24
|
+
ident = MediaTypeIdentifier.load('multipart/form-data')
|
25
|
+
[{ident.to_s => 'plain'}] # Multipart content type, but with the plain renderer (so there's no modification)
|
26
|
+
else
|
27
|
+
# TODO: We could run it through other handlers I guess...if they're registered
|
28
|
+
[{'application/json' => 'json'}]
|
29
|
+
end
|
30
|
+
|
31
|
+
h[:content] = MediaTypeObject.create_content_attribute_helper(type: attribute.type,
|
32
|
+
example_payload: attribute.example(nil),
|
33
|
+
example_handlers: example_handlers)
|
34
|
+
# # Key string (of MT) , value MTObject
|
35
|
+
# content_hash = info[:examples].each_with_object({}) do |(handler, example_hash),accum|
|
36
|
+
# content_type = example_hash[:content_type]
|
37
|
+
# accum[content_type] = MediaTypeObject.new(
|
38
|
+
# schema: dumped_schema, # Every MT will have the same exact type..oh well
|
39
|
+
# example: info[:examples][handler][:body],
|
40
|
+
# ).dump
|
41
|
+
# end
|
42
|
+
# # TODO! Handle Multipart types! they look like arrays now in the schema...etc
|
43
|
+
# h[:content] = content_hash
|
44
|
+
h
|
45
|
+
end
|
46
|
+
|
47
|
+
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require_relative 'media_type_object'
|
2
|
+
|
3
|
+
module Praxis
|
4
|
+
module Docs
|
5
|
+
module OpenApi
|
6
|
+
class ResponseObject
|
7
|
+
# https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#response-object
|
8
|
+
attr_reader :info
|
9
|
+
def initialize(info:)
|
10
|
+
@info = info
|
11
|
+
default_handlers = ApiDefinition.instance.info.produces
|
12
|
+
@output_handlers = Praxis::Application.instance.handlers.select do |k,v|
|
13
|
+
default_handlers.include?(k)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def dump_response_headers_object( headers )
|
18
|
+
headers.each_with_object({}) do |(name,data),accum|
|
19
|
+
# data is a hash with :value and :type keys
|
20
|
+
# How did we say in that must match a value in json schema again??
|
21
|
+
accum[name] = {
|
22
|
+
schema: SchemaObject.new(info: data[:type])
|
23
|
+
# allowed values: [ data[:value] ] ??? is this the right json schema way?
|
24
|
+
}
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def dump
|
29
|
+
data = {
|
30
|
+
description: info.description || ''
|
31
|
+
}
|
32
|
+
if headers_object = dump_response_headers_object( info.headers )
|
33
|
+
data[:headers] = headers_object
|
34
|
+
end
|
35
|
+
|
36
|
+
if info.media_type
|
37
|
+
|
38
|
+
identifier = MediaTypeIdentifier.load(info.media_type.identifier)
|
39
|
+
example_handlers = @output_handlers.each_with_object([]) do |(name, _handler), accum|
|
40
|
+
accum.push({ (identifier + name).to_s => name})
|
41
|
+
end
|
42
|
+
data[:content] = MediaTypeObject.create_content_attribute_helper(
|
43
|
+
type: info.media_type,
|
44
|
+
example_payload: info.example(nil),
|
45
|
+
example_handlers: example_handlers)
|
46
|
+
end
|
47
|
+
|
48
|
+
# if payload = info[:payload]
|
49
|
+
# body_type= payload[:id]
|
50
|
+
# raise "WAIT! response payload doesn't have an existing id for the schema!!! (do an if, and describe it if so)" unless body_type
|
51
|
+
# data[:schema] = {"$ref" => "#/definitions/#{body_type}" }
|
52
|
+
# end
|
53
|
+
|
54
|
+
|
55
|
+
# TODO: we do not support 'links'
|
56
|
+
data
|
57
|
+
end
|
58
|
+
|
59
|
+
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require_relative 'response_object'
|
2
|
+
|
3
|
+
module Praxis
|
4
|
+
module Docs
|
5
|
+
module OpenApi
|
6
|
+
class ResponsesObject
|
7
|
+
# https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#responses-object
|
8
|
+
attr_reader :responses
|
9
|
+
def initialize(responses:)
|
10
|
+
@responses = responses
|
11
|
+
end
|
12
|
+
|
13
|
+
|
14
|
+
def dump
|
15
|
+
# {
|
16
|
+
# "200": {
|
17
|
+
# "description": "a pet to be returned",
|
18
|
+
# "content": {
|
19
|
+
# "application/json": {
|
20
|
+
# "schema": {
|
21
|
+
# type: :object
|
22
|
+
# }
|
23
|
+
# }
|
24
|
+
# }
|
25
|
+
# },
|
26
|
+
# "default": {
|
27
|
+
# "description": "Unexpected error",
|
28
|
+
# "content": {
|
29
|
+
# "application/json": {
|
30
|
+
# "schema": {
|
31
|
+
# type: :object
|
32
|
+
# }
|
33
|
+
# }
|
34
|
+
# }
|
35
|
+
# }
|
36
|
+
# }
|
37
|
+
responses.each_with_object({}) do |(_response_name, response_definition), hash|
|
38
|
+
hash[response_definition.status.to_s] = ResponseObject.new(info: response_definition).dump
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
module Praxis
|
2
|
+
module Docs
|
3
|
+
module OpenApi
|
4
|
+
class SchemaObject
|
5
|
+
# https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#schema-object
|
6
|
+
attr_reader :type, :attribute
|
7
|
+
def initialize(info:)
|
8
|
+
#info could be an attribute ... or a type?
|
9
|
+
if info.is_a? Attributor::Attribute
|
10
|
+
@attribute = info
|
11
|
+
else
|
12
|
+
@type = info
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def dump_example
|
17
|
+
ex = \
|
18
|
+
if attribute
|
19
|
+
attribute.example
|
20
|
+
else
|
21
|
+
type.example
|
22
|
+
end
|
23
|
+
ex.respond_to?(:dump) ? ex.dump : ex
|
24
|
+
end
|
25
|
+
|
26
|
+
def dump_schema
|
27
|
+
if attribute
|
28
|
+
attribute.as_json_schema(shallow: true, example: nil)
|
29
|
+
else
|
30
|
+
type.as_json_schema(shallow: true, example: nil)
|
31
|
+
end
|
32
|
+
# # TODO: FIXME: return a generic object type if the passed info was weird.
|
33
|
+
# return { type: :object } unless info
|
34
|
+
|
35
|
+
# h = {
|
36
|
+
# #type: convert_family_to_json_type( info[:type] )
|
37
|
+
# type: info[:type]
|
38
|
+
# #TODO: format?
|
39
|
+
# }
|
40
|
+
# # required prop!!!??
|
41
|
+
# h[:default] = info[:default] if info[:default]
|
42
|
+
# h[:pattern] = info[:regexp] if info[:regexp]
|
43
|
+
# # TODO: there are other possible things we can do..maximum, minimum...etc
|
44
|
+
|
45
|
+
# if h[:type] == :array
|
46
|
+
# # FIXME: ... hack it for MultiPart arrays...where there's no member attr
|
47
|
+
# member_type = info[:type][:member_attribute]
|
48
|
+
# unless member_type
|
49
|
+
# member_type = { family: :hash}
|
50
|
+
# end
|
51
|
+
# h[:items] = SchemaObject.new(info: member_type ).dump_schema
|
52
|
+
# end
|
53
|
+
# h
|
54
|
+
rescue => e
|
55
|
+
puts "Error dumping schema #{e}"
|
56
|
+
end
|
57
|
+
|
58
|
+
def convert_family_to_json_type( praxis_type )
|
59
|
+
case praxis_type[:family].to_sym
|
60
|
+
when :string
|
61
|
+
:string
|
62
|
+
when :hash
|
63
|
+
:object
|
64
|
+
when :array #Warning! Multipart types are arrays!
|
65
|
+
:array
|
66
|
+
when :numeric
|
67
|
+
case praxis_type[:id]
|
68
|
+
when 'Attributor-Integer'
|
69
|
+
:integer
|
70
|
+
when 'Attributor-BigDecimal'
|
71
|
+
:integer
|
72
|
+
when 'Attributor-Float'
|
73
|
+
:number
|
74
|
+
end
|
75
|
+
when :temporal
|
76
|
+
:string
|
77
|
+
when :boolean
|
78
|
+
:boolean
|
79
|
+
else
|
80
|
+
raise "Unknown praxis family type: #{praxis_type[:family]}"
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Praxis
|
2
|
+
module Docs
|
3
|
+
module OpenApi
|
4
|
+
class ServerObject
|
5
|
+
# https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#server-object
|
6
|
+
attr_reader :url, :description, :variables
|
7
|
+
def initialize(url: , description: nil, variables: [])
|
8
|
+
@url = url
|
9
|
+
@description = description
|
10
|
+
@variables = variables
|
11
|
+
raise "OpenApi docs require a 'url' for your server object." unless url
|
12
|
+
end
|
13
|
+
|
14
|
+
def dump
|
15
|
+
result = {url: url}
|
16
|
+
result[:description] = description if description
|
17
|
+
result[:variables] = variables unless variables.empty?
|
18
|
+
|
19
|
+
result
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Praxis
|
2
|
+
module Docs
|
3
|
+
module OpenApi
|
4
|
+
class TagObject
|
5
|
+
attr_reader :name, :description
|
6
|
+
def initialize(name:,description: )
|
7
|
+
@name = name
|
8
|
+
@description = description
|
9
|
+
end
|
10
|
+
|
11
|
+
def dump
|
12
|
+
{
|
13
|
+
name: name,
|
14
|
+
description: description,
|
15
|
+
#externalDocs: ???,
|
16
|
+
}
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
data/lib/praxis/links.rb
CHANGED
@@ -42,10 +42,6 @@ module Praxis::Mapper
|
|
42
42
|
association[:local_key_columns].each {|col| add_select(col) }
|
43
43
|
|
44
44
|
node = SelectorGeneratorNode.new(associated_resource)
|
45
|
-
if association[:remote_key_columns].nil?
|
46
|
-
binding.pry
|
47
|
-
puts association
|
48
|
-
end
|
49
45
|
unless association[:remote_key_columns].empty?
|
50
46
|
# Make sure we add the required columns for this association to the remote model query
|
51
47
|
fields = {} if fields == true
|
@@ -28,6 +28,10 @@ module Praxis
|
|
28
28
|
self
|
29
29
|
end
|
30
30
|
|
31
|
+
def self.json_schema_type
|
32
|
+
:object
|
33
|
+
end
|
34
|
+
|
31
35
|
def self.example(context=nil, options:{})
|
32
36
|
if (payload_attribute = options[:payload_attribute])
|
33
37
|
payload = payload_attribute.example(context + ['payload'])
|
@@ -52,8 +56,7 @@ module Praxis
|
|
52
56
|
headers_attribute: headers_attribute,
|
53
57
|
filename_attribute: filename_attribute)
|
54
58
|
end
|
55
|
-
|
56
|
-
|
59
|
+
|
57
60
|
def self.describe(shallow=true, example: nil, options:{})
|
58
61
|
hash = super(shallow, example: example)
|
59
62
|
|
@@ -62,5 +62,28 @@ namespace :praxis do
|
|
62
62
|
generator.save!
|
63
63
|
end
|
64
64
|
|
65
|
+
desc "Generate OpenAPI 3 docs for a Praxis App"
|
66
|
+
task :openapi => [:environment] do |t, args|
|
67
|
+
require 'fileutils'
|
68
|
+
|
69
|
+
Praxis::Blueprint.caching_enabled = false
|
70
|
+
generator = Praxis::Docs::OpenApiGenerator.new(Dir.pwd)
|
71
|
+
generator.save!
|
72
|
+
end
|
73
|
+
|
74
|
+
desc "Preview (and Generate) OpenAPI 3 docs for a Praxis App"
|
75
|
+
task :openapipreview => [:openapi] do |t, args|
|
76
|
+
require 'webrick'
|
77
|
+
docs_port = 9090
|
78
|
+
root = Dir.pwd + '/docs/openapi/'
|
79
|
+
wb = Thread.new do
|
80
|
+
s = WEBrick::HTTPServer.new(:Port => docs_port, :DocumentRoot => root)
|
81
|
+
trap('INT') { s.shutdown }
|
82
|
+
s.start
|
83
|
+
end
|
84
|
+
`open http://localhost:#{docs_port}/`
|
85
|
+
wb.join
|
86
|
+
end
|
87
|
+
|
65
88
|
end
|
66
89
|
end
|
@@ -14,6 +14,16 @@ module Praxis
|
|
14
14
|
hash
|
15
15
|
end
|
16
16
|
|
17
|
+
def as_json_schema(**args)
|
18
|
+
the_type = @attribute && @attribute.type || member_type
|
19
|
+
the_type.as_json_schema(args)
|
20
|
+
end
|
21
|
+
|
22
|
+
def json_schema_type
|
23
|
+
the_type = @attribute && @attribute.type || member_type
|
24
|
+
the_type.json_schema_type
|
25
|
+
end
|
26
|
+
|
17
27
|
def description(text=nil)
|
18
28
|
@description = text if text
|
19
29
|
@description
|
@@ -153,6 +153,68 @@ module Praxis
|
|
153
153
|
example
|
154
154
|
end
|
155
155
|
|
156
|
+
def self.json_schema_type
|
157
|
+
:object
|
158
|
+
end
|
159
|
+
|
160
|
+
# Multipart request bodies are special in OPEN API
|
161
|
+
# schema: # Request payload
|
162
|
+
# type: object
|
163
|
+
# properties: # Request parts
|
164
|
+
# id: # Part 1 (string value)
|
165
|
+
# type: string
|
166
|
+
# format: uuid
|
167
|
+
# address: # Part2 (object)
|
168
|
+
# type: object
|
169
|
+
# properties:
|
170
|
+
# street:
|
171
|
+
# type: string
|
172
|
+
# city:
|
173
|
+
# type: string
|
174
|
+
# profileImage: # Part 3 (an image)
|
175
|
+
# type: string
|
176
|
+
# format: binary
|
177
|
+
#
|
178
|
+
# NOTE: not sure if this
|
179
|
+
def self.as_openapi_request_body( attribute_options: {} )
|
180
|
+
hash = { type: json_schema_type }
|
181
|
+
opts = self.options.merge( attribute_options )
|
182
|
+
hash[:description] = opts[:description] if opts[:description]
|
183
|
+
hash[:default] = opts[:default] if opts[:default]
|
184
|
+
|
185
|
+
unless self.attributes.empty?
|
186
|
+
props = {}
|
187
|
+
encoding = {}
|
188
|
+
self.attributes.each do |part_name, part_attribute|
|
189
|
+
part_example = part_attribute.example
|
190
|
+
key_to_use = part_name.is_a?(Regexp) ? part_name.source : part_name
|
191
|
+
|
192
|
+
part_info = {}
|
193
|
+
if (payload_attribute = part_attribute.options[:payload_attribute])
|
194
|
+
props[key_to_use] = payload_attribute.as_json_schema(example: part_example.payload)
|
195
|
+
end
|
196
|
+
#{
|
197
|
+
# contentType: 'fff',
|
198
|
+
# headers: {
|
199
|
+
# custom1: 'safd'
|
200
|
+
# }
|
201
|
+
if (headers_attribute = part_attribute.options[:headers_attribute])
|
202
|
+
# Does this 'Content-Type' string check work?...can it be a symbol? what does it mean anyway?
|
203
|
+
encoding[key_to_use][:contentType] = headers_attribute['Content-Type'] if headers_attribute['Content-Type']
|
204
|
+
# TODO?rethink? ...is this correct?: att a 'headers' key with some header schemas if this part have some
|
205
|
+
encoding[key_to_use]['headers'] = headers_attribute.as_json_schema(example: part_example.headers)
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
hash[:properties] = props
|
210
|
+
hash[:encoding] = encoding unless encoding.empty?
|
211
|
+
end
|
212
|
+
hash
|
213
|
+
end
|
214
|
+
|
215
|
+
def self.as_json_schema( shallow: false, example: nil, attribute_options: {} )
|
216
|
+
as_openapi_request_body(attribute_options: attribute_options)
|
217
|
+
end
|
156
218
|
|
157
219
|
def self.describe(shallow=true, example: nil)
|
158
220
|
type_name = Attributor.type_name(self)
|
data/lib/praxis/version.rb
CHANGED
data/praxis.gemspec
CHANGED
@@ -24,8 +24,8 @@ Gem::Specification.new do |spec|
|
|
24
24
|
spec.add_dependency 'mustermann', '>=1.1', '<=2'
|
25
25
|
spec.add_dependency 'activesupport', '>= 3'
|
26
26
|
spec.add_dependency 'mime', '~> 0'
|
27
|
-
spec.add_dependency 'praxis-blueprints', '>= 3.
|
28
|
-
spec.add_dependency 'attributor', '>= 5.
|
27
|
+
spec.add_dependency 'praxis-blueprints', '>= 3.5'
|
28
|
+
spec.add_dependency 'attributor', '>= 5.5'
|
29
29
|
spec.add_dependency 'thor'
|
30
30
|
spec.add_dependency 'terminal-table', '~> 1.4'
|
31
31
|
|
@@ -514,12 +514,11 @@ describe Praxis::ResponseDefinition do
|
|
514
514
|
|
515
515
|
context 'for a definition with a media type' do
|
516
516
|
let(:media_type) { Instance }
|
517
|
-
subject(:payload) { output[:payload] }
|
517
|
+
subject(:payload) { output[:payload][:type] }
|
518
518
|
|
519
519
|
before do
|
520
520
|
response.media_type Instance
|
521
521
|
end
|
522
|
-
|
523
522
|
its([:name]) { should eq 'Instance' }
|
524
523
|
context 'examples' do
|
525
524
|
subject(:examples) { payload[:examples] }
|
@@ -571,7 +570,9 @@ describe Praxis::ResponseDefinition do
|
|
571
570
|
end
|
572
571
|
|
573
572
|
it{ should be_kind_of(::Hash) }
|
574
|
-
|
573
|
+
it 'has the right type info' do
|
574
|
+
expect(subject[:payload][:type]).to match(id: 'Praxis-SimpleMediaType', name: 'Praxis::SimpleMediaType', family: 'string', identifier: 'foobar')
|
575
|
+
end
|
575
576
|
its([:status]){ should == 200 }
|
576
577
|
end
|
577
578
|
context 'using a full response definition block' do
|
@@ -587,7 +588,9 @@ describe Praxis::ResponseDefinition do
|
|
587
588
|
end
|
588
589
|
|
589
590
|
it{ should be_kind_of(::Hash) }
|
590
|
-
|
591
|
+
it 'has the right type info' do
|
592
|
+
expect(subject[:payload][:type]).to match(id: 'Praxis-SimpleMediaType', name: 'Praxis::SimpleMediaType', family: 'string', identifier: 'custom_media')
|
593
|
+
end
|
591
594
|
its([:status]) { should == 234 }
|
592
595
|
end
|
593
596
|
end
|
data/spec/spec_app/design/api.rb
CHANGED
@@ -27,6 +27,12 @@ Praxis::ApiDefinition.define do
|
|
27
27
|
produces 'json','xml'
|
28
28
|
#version_with :path
|
29
29
|
#base_path "/v:api_version"
|
30
|
+
|
31
|
+
# Custom attributes (for OpenApi, for example)
|
32
|
+
termsOfService "http://example.com/tos"
|
33
|
+
contact name: 'Joe', email: 'joe@email.com'
|
34
|
+
license name: "Apache 2.0",
|
35
|
+
url: "https://www.apache.org/licenses/LICENSE-2.0.html"
|
30
36
|
end
|
31
37
|
|
32
38
|
info '1.0' do # Applies to 1.0 version (and inherits everything else form the global one)
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: praxis
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.0.pre.
|
4
|
+
version: 2.0.pre.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Josep M. Blanquer
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2020-08-
|
12
|
+
date: 2020-08-21 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rack
|
@@ -79,28 +79,28 @@ dependencies:
|
|
79
79
|
requirements:
|
80
80
|
- - ">="
|
81
81
|
- !ruby/object:Gem::Version
|
82
|
-
version: '3.
|
82
|
+
version: '3.5'
|
83
83
|
type: :runtime
|
84
84
|
prerelease: false
|
85
85
|
version_requirements: !ruby/object:Gem::Requirement
|
86
86
|
requirements:
|
87
87
|
- - ">="
|
88
88
|
- !ruby/object:Gem::Version
|
89
|
-
version: '3.
|
89
|
+
version: '3.5'
|
90
90
|
- !ruby/object:Gem::Dependency
|
91
91
|
name: attributor
|
92
92
|
requirement: !ruby/object:Gem::Requirement
|
93
93
|
requirements:
|
94
94
|
- - ">="
|
95
95
|
- !ruby/object:Gem::Version
|
96
|
-
version: '5.
|
96
|
+
version: '5.5'
|
97
97
|
type: :runtime
|
98
98
|
prerelease: false
|
99
99
|
version_requirements: !ruby/object:Gem::Requirement
|
100
100
|
requirements:
|
101
101
|
- - ">="
|
102
102
|
- !ruby/object:Gem::Version
|
103
|
-
version: '5.
|
103
|
+
version: '5.5'
|
104
104
|
- !ruby/object:Gem::Dependency
|
105
105
|
name: thor
|
106
106
|
requirement: !ruby/object:Gem::Requirement
|
@@ -540,6 +540,18 @@ files:
|
|
540
540
|
- lib/praxis/dispatcher.rb
|
541
541
|
- lib/praxis/docs/generator.rb
|
542
542
|
- lib/praxis/docs/link_builder.rb
|
543
|
+
- lib/praxis/docs/open_api_generator.rb
|
544
|
+
- lib/praxis/docs/openapi/info_object.rb
|
545
|
+
- lib/praxis/docs/openapi/media_type_object.rb
|
546
|
+
- lib/praxis/docs/openapi/operation_object.rb
|
547
|
+
- lib/praxis/docs/openapi/parameter_object.rb
|
548
|
+
- lib/praxis/docs/openapi/paths_object.rb
|
549
|
+
- lib/praxis/docs/openapi/request_body_object.rb
|
550
|
+
- lib/praxis/docs/openapi/response_object.rb
|
551
|
+
- lib/praxis/docs/openapi/responses_object.rb
|
552
|
+
- lib/praxis/docs/openapi/schema_object.rb
|
553
|
+
- lib/praxis/docs/openapi/server_object.rb
|
554
|
+
- lib/praxis/docs/openapi/tag_object.rb
|
543
555
|
- lib/praxis/error_handler.rb
|
544
556
|
- lib/praxis/exception.rb
|
545
557
|
- lib/praxis/exceptions/config.rb
|