require_relative 'open_api/info_object.rb'
require_relative 'open_api/server_object.rb'
require_relative 'open_api/paths_object.rb'
require_relative 'open_api/tag_object.rb'
module Praxis
module Docs
class OpenApiGenerator
require 'active_support/core_ext/enumerable' # For index_by
API_DOCS_DIRNAME = 'docs/openapi'
EXCLUDED_TYPES_FROM_OUTPUT = Set.new([
Attributor::Boolean,
Attributor::CSV,
Attributor::DateTime,
Attributor::Date,
Attributor::Float,
Attributor::Hash,
Attributor::Ids,
Attributor::Integer,
Attributor::Object,
Attributor::String,
Attributor::Symbol,
Attributor::URI,
]).freeze
attr_reader :resources_by_version, :types_by_id, :infos_by_version, :doc_root_dir
# substitutes ":params_like_so" for {params_like_so}
def self.templatize_url( string )
Mustermann.new(string).to_templates.first
end
def save!
initialize_directories
# Restrict the versions listed in the index file to the ones for which we have at least 1 resource
write_index_file( for_versions: resources_by_version.keys )
resources_by_version.keys.each do |version|
write_version_file(version)
end
end
def initialize(root)
require 'yaml'
@root = root
@resources_by_version = Hash.new do |h,k|
h[k] = Set.new
end
@infos = ApiDefinition.instance.infos
collect_resources
collect_types
end
private
def collect_resources
# load all resource definitions registered with Praxis
Praxis::Application.instance.endpoint_definitions.map do |resource|
# skip resources with doc_visibility of :none
next if resource.metadata[:doc_visibility] == :none
version = resource.version
# TODO: it seems that we shouldn't hardcode n/a in Praxis
# version = "unversioned" if version == "n/a"
@resources_by_version[version] << resource
end
end
def collect_types
@types_by_id = ObjectSpace.each_object( Class ).select do |obj|
obj < Attributor::Type
end.index_by(&:id)
end
def write_index_file( for_versions: )
# TODO. create a simple html file that can link to the individual versions available
end
def scan_types_for_version(version, dumped_resources)
found_media_types = resources_by_version[version].select{|r| r.media_type}.collect {|r| r.media_type.describe }
# We'll start by processing the rendered mediatypes
processed_types = Set.new(resources_by_version[version].select do|r|
r.media_type && !r.media_type.is_a?(Praxis::SimpleMediaType)
end.collect(&:media_type))
newfound = Set.new
found_media_types.each do |mt|
newfound += scan_dump_for_types( { type: mt} , processed_types )
end
# Then will process the rendered resources (noting)
newfound += scan_dump_for_types( dumped_resources, Set.new )
# At this point we've done a scan of the dumped resources and mediatypes.
# In that scan we've discovered a bunch of types, however, many of those might have appeared in the JSON
# rendered in just shallow mode, so it is not guaranteed that we've seen all the available types.
# For that we'll do a (non-shallow) dump of all the types we found, and scan them until the scans do not
# yield types we haven't seen before
while !newfound.empty? do
dumped = newfound.collect(&:describe)
processed_types += newfound
newfound = scan_dump_for_types( dumped, processed_types )
end
processed_types
end
def scan_dump_for_types( data, processed_types )
newfound_types = Set.new
case data
when Array
data.collect{|item| newfound_types += scan_dump_for_types( item , processed_types ) }
when Hash
if data.key?(:type) && data[:type].kind_of?(Hash) && ( [:id,:name,:family] - data[:type].keys ).empty?
type_id = data[:type][:id]
unless type_id.nil? || type_id == Praxis::SimpleMediaType.id #SimpleTypes shouldn't be collected
unless types_by_id[type_id]
raise "Error! We have detected a reference to a 'Type' with id='#{type_id}' which is not derived from Attributor::Type" +
" Document generation cannot proceed."
end
newfound_types << types_by_id[type_id] unless processed_types.include? types_by_id[type_id]
end
end
data.values.map{|item| newfound_types += scan_dump_for_types( item , processed_types)}
end
newfound_types
end
def write_version_file( version )
# version_info = infos_by_version[version]
# # Hack, let's "inherit/copy" all traits of a version from the global definition
# # Eventually traits should be defined for a version (and inheritable from global) so we'll emulate that here
# version_info[:traits] = infos_by_version[:traits]
dumped_resources = dump_resources( resources_by_version[version] )
processed_types = scan_types_for_version(version, dumped_resources)
# Here we have:
# processed types: which includes mediatypes and normal types...real classes
# processed resources for this version: resources_by_version[version]
info_object = OpenApi::InfoObject.new(version: version, api_definition_info: @infos[version])
# We only support a server in Praxis ... so we'll use the base path
server_object = OpenApi::ServerObject.new( url: @infos[version].base_path )
paths_object = OpenApi::PathsObject.new( resources: resources_by_version[version])
full_data = {
openapi: "3.0.2",
info: info_object.dump,
servers: [server_object.dump],
paths: paths_object.dump,
# responses: {}, #TODO!! what do we get here? the templates?...need to transform to "Responses Definitions Object"
# securityDefinitions: {}, # NOTE: No security definitions in Praxis
# security: [], # NOTE: No security definitions in Praxis
}
# Create the top level tags by:
# 1- First adding all the resource display names (and descriptions)
tags_for_resources = resources_by_version[version].collect do |resource|
OpenApi::TagObject.new(name: resource.display_name, description: resource.description ).dump
end
full_data[:tags] = tags_for_resources
# 2- Then adding all of the top level traits but marking them special with the x-traitTag (of Redoc)
tags_for_traits = (ApiDefinition.instance.traits).collect do |name, info|
OpenApi::TagObject.new(name: name, description: info.description).dump.merge(:'x-traitTag' => true)
end
unless tags_for_traits.empty?
full_data[:tags] = full_data[:tags] + tags_for_traits
end
# Include only MTs (i.e., not custom types or simple types...)
component_schemas = reusable_schema_objects(processed_types.select{|t| t < Praxis::MediaType})
# 3- Then adding all of the top level Mediatypes...so we can present them at the bottom, otherwise they don't show
tags_for_mts = component_schemas.map do |(name, info)|
special_redoc_anchor = "