praxis 2.0.pre.4 → 2.0.pre.5
Sign up to get free protection for your applications and to get access to all the features.
- 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
|