class Praxis::Docs::Generator

Constants

API_DOCS_DIRNAME
EXCLUDED_TYPES_FROM_OUTPUT

Attributes

doc_root_dir[R]
infos_by_version[R]
resources_by_version[R]
types_by_id[R]

Public Class Methods

new(root) click to toggle source
# File lib/praxis/docs/generator.rb, line 27
def initialize(root)
  require 'yaml'
  @resources_by_version =  Hash.new do |h,k|
    h[k] = Set.new
  end
  initialize_directories(root)

  Attributor::AttributeResolver.current = Attributor::AttributeResolver.new
  collect_infos
  collect_resources
  collect_types
end

Public Instance Methods

save!() click to toggle source
# File lib/praxis/docs/generator.rb, line 40
def save!
  # 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

Private Instance Methods

collect_infos() click to toggle source
# File lib/praxis/docs/generator.rb, line 76
def collect_infos
  # All infos. Including keys for `:global`, "n/a", and any string version
  @infos_by_version = ApiDefinition.instance.describe
end
collect_resources() click to toggle source
# File lib/praxis/docs/generator.rb, line 58
def collect_resources
  # load all resource definitions registered with Praxis
  Praxis::Application.instance.resource_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
collect_types() click to toggle source
# File lib/praxis/docs/generator.rb, line 70
def collect_types
  @types_by_id = ObjectSpace.each_object( Class ).select do |obj|
    obj < Attributor::Type
  end.index_by(&:id)
end
dump_example_for(context_name, object) click to toggle source
# File lib/praxis/docs/generator.rb, line 224
def dump_example_for(context_name, object)
  example = object.example(Array(context_name))
  if object.is_a? Praxis::Blueprint
    example.render(view: :master)
  elsif object.is_a? Attributor::Attribute
    object.dump(example)
  else
    raise "Do not know how to dump this object (it is not a Blueprint or an Attribute): #{object}"
  end
end
dump_resources( resources ) click to toggle source
# File lib/praxis/docs/generator.rb, line 170
def dump_resources( resources )
  resources.each_with_object({}) do |r, hash|
    # Do not report undocumentable resources
    next if r.metadata[:doc_visibility] == :none
    context = [r.id]
    resource_description = r.describe(context: context)

    # strip actions with doc_visibility of :none
    resource_description[:actions].reject! { |a| a[:metadata][:doc_visibility] == :none }

    # Go through the params/payload of each action and augment them by
    # adding a generated example (then stick it into the description hash)
    r.actions.each do |action_name, action|
      # skip actions with doc_visibility of :none
      next if action.metadata[:doc_visibility] == :none

      action_description = resource_description[:actions].find {|a| a[:name] == action_name }
    end

    hash[r.id] = resource_description
  end
end
dump_schemas(types) click to toggle source
# File lib/praxis/docs/generator.rb, line 194
def dump_schemas(types)
  reportable_types = types - EXCLUDED_TYPES_FROM_OUTPUT
  reportable_types.each_with_object({}) do |type, array|
    next if ( type.respond_to?(:anonymous?) && type.anonymous? )

    context = [type.id]
    example_data = type.example(context)
    type_output = type.describe(false, example: example_data)

    type_output[:display_name] = type.display_name if type.respond_to?(:display_name)
    unless type_output[:display_name]
      # For non MediaTypes or pure types or anonymous types fallback to their name, and worst case to their id
      type_output[:display_name] = type_output[:name] || type_output[:id]
    end
    if type_output[:views]
      type_output[:views].delete(:master)
      type_output[:views].each do |view_name, view_info|
        view_info[:example] = example_data.render(view: view_name)
      end
    end
    type_output[:example] = if example_data.respond_to? :render
      example_data.render(view: :master)
    else
      type.dump(example_data)
    end
    array[type.id] = type_output
  end
end
initialize_directories(root) click to toggle source
# File lib/praxis/docs/generator.rb, line 50
def initialize_directories(root)
  @doc_root_dir = File.join(root, API_DOCS_DIRNAME)

  # remove previous data (and reset the directory)
  FileUtils.rm_rf @doc_root_dir if File.exists?(@doc_root_dir)
  FileUtils.mkdir_p @doc_root_dir unless File.exists? @doc_root_dir
end
scan_dump_for_types( data, processed_types ) click to toggle source

Data: hash/array structure of dumped resources and/or types processed_types: list of type classes that have already gone through a describe+collect (this or previous rounds) … any processed type won't need to be described+reached any longer newly_found: list of type classes that have been seen in the search (and that weren't already in the processed type)

# File lib/praxis/docs/generator.rb, line 86
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
write_index_file( for_versions: ) click to toggle source
# File lib/praxis/docs/generator.rb, line 107
def write_index_file( for_versions:  )
  # Gather the versions
  versions = infos_by_version.keys.reject{|v| v == :global || v == :traits || !for_versions.include?(v) }.map do |version|
    version == "n/a" ? "unversioned" : version
  end
  data = {
    info: infos_by_version[:global][:info],
    versions: versions
    # Note, I don't think we need to report the global traits (but rather the ones in the version)
  }
  filename = File.join(doc_root_dir, "index-new.json")
  puts "Generating Index file: #{filename}"
  File.open(filename, 'w') {|f| f.write(JSON.pretty_generate(data))}
end
write_version_file( version ) click to toggle source
# File lib/praxis/docs/generator.rb, line 122
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] )
  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

  dumped_schemas = dump_schemas( processed_types )
  full_data = {
    info: version_info[:info],
    resources: dumped_resources,
    schemas: dumped_schemas,
    traits: version_info[:traits] || []
  }
  # Write the file
  version_file = ( version == "n/a" ? "unversioned" : version )
  filename = File.join(doc_root_dir, version_file)

  puts "Generating API file: #{filename} (in json and yaml)"
  File.open(filename+".json", 'w') {|f| f.write(JSON.pretty_generate(full_data))}
  File.open(filename+".yml", 'w') {|f| f.write(YAML.dump(full_data))}
end