require 'fileutils' module YARD module Server # This exception is raised when {LibraryVersion#prepare!} fails, or discovers # that the library is not "prepared" to be served by class LibraryNotPreparedError < RuntimeError; end # A library version encapsulates a library's documentation at a specific version. # Although the version is optional, this allows for creating multiple documentation # points for a specific library, each representing a unique version. The term # "library" used in other parts of the YARD::Server documentation refers to # objects of this class unless otherwise noted. # # A library points to a location where a {#yardoc_file} is located so that # its documentation may be loaded and served. Optionally, a {#source_path} is # given to point to a location where any extra files (and {YARD::CLI::Yardoc .yardopts}) # should be loaded from. Both of these methods may not be known immediately, # since the yardoc file may not be built until later. Resolving the yardoc # file and source path are dependent on the specific library "source type" used. # Source types (known as "library source") are discussed in detail below. # # == Using with Adapters # A list of libraries need to be passed into adapters upon creation. In # most cases, you will never do this manually, but if you use a {RackMiddleware}, # you will need to pass in this list yourself. To build this list of libraries, # you should create a hash of library names mapped to an *Array* of LibraryVersion # objects. For example: # # {'mylib' => [LibraryVersion.new('mylib', '1.0', ...), # LibraryVersion.new('mylib', '2.0', ...)]} # # Note that you can also use {Adapter#add_library} for convenience. # # The "array" part is required, even for just one library version. # # == Library Sources # The {#source} method represents the library source type, ie. where the # library "comes from". It might come from "disk", or it might come from a # "gem" (technically the disk, but a separate type nonetheless). In these # two cases, the yardoc file sits somewhere on your filesystem, though # it may also be built dynamically if it does not yet exist. This behaviour # is controlled through the {#prepare!} method, which prepares the yardoc file # given a specific library source. We will see how this works in detail in # the following section. # # == Implementing a Custom Library Source # YARD can be extended to support custom library sources in order to # build or retrieve a yardoc file at runtime from many different locations. # # To implement this behaviour, two methods must be added to the +LibraryVersion+ # class, +#load_yardoc_from_SOURCE+ and +#source_path_for_SOURCE+. In both # cases, "SOURCE" represents the source type used in {#source} when creating # the library object. The +#source_path_for_SOURCE+ method is called upon # creation and should return the location where the source code for the library # lives. The load method is called from {#prepare!} if there is no yardoc file # and should set {#yardoc_file}. Below is a full example for # implementing a custom library source, +:http+, which reads packaged .yardoc # databases from zipped archives off of an HTTP server. # # @example Implementing a Custom Library Source # # Adds the source type "http" for .yardoc files zipped on HTTP servers # class LibraryVersion # def load_yardoc_from_http # return if yardoc_file # we have the library # # # otherwise download it in a thread and return immediately # Thread.new do # # zip/unzip method implementations are not shown # download_zip_file("http://mysite.com/yardocs/#{self}.zip") # unzip_file_to("/path/to/yardocs/#{self}") # self.yardoc_file = "/path/to/yardocs/#{self}/.yardoc" # self.source_path = self.yardoc_file # end # # # tell the server it's not ready yet (but it might be next time) # raise LibraryNotPreparedError # end # # # we set this later # def source_path_for_http; nil end # end # # # Creating a library of this source type: # LibraryVersion.new('name', '1.0', nil, :http) # class LibraryVersion # @return [String] the name of the library attr_accessor :name # @return [String] the version of the specific library attr_accessor :version # @return [String] the location of the yardoc file used to load the object # information from. # @return [nil] if no yardoc file exists yet. In this case, {#prepare!} will # be called on this library to build the yardoc file. attr_accessor :yardoc_file # @return [Symbol] the source type representing where the yardoc should be # loaded from. Defaults are +:disk+ and +:gem+, though custom sources # may be implemented. This value is used to inform {#prepare!} about how # to load the necessary data in order to display documentation for an object. # @see LibraryVersion LibraryVersion documentation for "Implementing a Custom Library Source" attr_accessor :source # @return [String] the location of the source code for a library. This # value is filled by calling +#source_path_for_SOURCE+ on this class. # @return [nil] if there is no source code # @see LibraryVersion LibraryVersion documentation for "Implementing a Custom Library Source" attr_accessor :source_path # @param [String] name the name of the library # @param [String] version the specific (usually, but not always, numeric) library # version # @param [String] yardoc the location of the yardoc file, or nil if it is # generated later # @param [Symbol] source the location of the files used to build the yardoc. # Builtin source types are +:disk+ or +:gem+. def initialize(name, version = nil, yardoc = nil, source = :disk) self.name = name self.yardoc_file = yardoc self.version = version self.source = source self.source_path = load_source_path end # @param [Boolean] url_format if true, returns the string in a URI-compatible # format (for appending to a URL). Otherwise, it is given in a more human # readable format. # @return [String] the string representation of the library. def to_s(url_format = true) version ? "#{name}#{url_format ? '/' : '-'}#{version}" : "#{name}" end # @return [Fixnum] used for Hash mapping. def hash; to_s.hash end # @return [Boolean] whether another LibraryVersion is equal to this one def eql?(other) other.is_a?(LibraryVersion) && other.name == name && other.version == version && other.yardoc_file == yardoc_file end alias == eql? alias equal? eql? # @note You should not directly override this method. Instead, implement # +load_yardoc_from_SOURCENAME+ when implementing loading for a specific # source type. See the {LibraryVersion} documentation for "Implementing # a Custom Library Source" # # Prepares a library to be displayed by the server. This callback is # performed before each request on a library to ensure that it is loaded # and ready to be viewed. If any steps need to be performed prior to loading, # they are performed through this method (though they should be implemented # through the +load_yardoc_from_SOURCE+ method). # # @raise [LibraryNotPreparedError] if the library is not ready to be # displayed. Usually when raising this error, you would simultaneously # begin preparing the library for subsequent requests, although this # is not necessary. def prepare! return if yardoc_file meth = "load_yardoc_from_#{source}" send(meth) if respond_to?(meth, true) end # @return [Gem::Specification] a gemspec object for a given library. Used # for :gem source types. # @return [nil] if there is no installed gem for the library def gemspec ver = version ? "= #{version}" : ">= 0" Gem.source_index.find_name(name, ver).first end protected # Called when a library of source type "disk" is to be prepared. In this # case, the {#yardoc_file} should already be set, so nothing needs to be # done. def load_yardoc_from_disk nil end # Called when a library of source type "gem" is to be prepared. In this # case, the {#yardoc_file} needs to point to the correct location for # the installed gem. The yardoc file is built if it has not been done. # # @raise [LibraryNotPreparedError] if the gem does not have an existing # yardoc file. def load_yardoc_from_gem require 'rubygems' ver = version ? "= #{version}" : ">= 0" self.yardoc_file = Registry.yardoc_file_for_gem(name, ver) unless yardoc_file && File.directory?(yardoc_file) Thread.new do # Build gem docs on demand log.debug "Building gem docs for #{to_s(false)}" CLI::Gems.run(name, ver) self.yardoc_file = Registry.yardoc_file_for_gem(name, ver) FileUtils.touch(File.join(yardoc_file, 'complete')) end end unless yardoc_file && File.exist?(File.join(yardoc_file, 'complete')) raise LibraryNotPreparedError end end # @return [String] the source path for a disk source def source_path_for_disk File.dirname(yardoc_file) if yardoc_file end # @return [String] the source path for a gem source def source_path_for_gem gemspec.full_gem_path if gemspec end private def load_source_path meth = "source_path_for_#{source}" send(meth) if respond_to?(meth, true) end end end end