lib/solargraph/yard_map/core_docs.rb



# frozen_string_literal: true


require 'net/http'
require 'uri'
require 'json'
require 'fileutils'

module Solargraph
  class YardMap
    # Tools for managing core documentation.

    #

    module CoreDocs
      # The URL for downloading core documentation

      SOURCE = 'https://solargraph.org/download'

      # The default core documentation version

      DEFAULT = '2.2.2'

      class << self
        # The directory where core documentation is installed.

        #

        # @return [String]

        def cache_dir
          # The directory is not stored in a variable so it can be overridden

          # in specs.

          ENV['SOLARGRAPH_CACHE'] || File.join(Dir.home, '.solargraph', 'cache')
        end

        # Ensure installation of minimum documentation.

        #

        # @return [void]

        def require_minimum
          return unless best_match.nil?
          FileUtils.mkdir_p cache_dir
          FileUtils.cp File.join(Solargraph::YARDOC_PATH, "#{DEFAULT}.tar.gz"), cache_dir
          install_archive File.join(cache_dir, "#{DEFAULT}.tar.gz")
        end

        # True if core documentation is installed for the specified version

        # number.

        #

        # @param ver [String] The version number to check

        # @return [Boolean]

        def valid?(ver)
          dir = File.join(cache_dir, ver)
          return false unless File.directory?(dir)
          return false unless File.directory?(File.join(dir, 'yardoc'))
          return false unless File.directory?(File.join(dir, 'yardoc-stdlib'))
          true
        end

        # Get a list of version numbers for currently installed core

        # documentation.

        #

        # @return [Array<String>] The installed version numbers

        def versions
          dirs = Dir[File.join(cache_dir, '*')].map{|d| File.basename(d)}
          dirs.keep_if{|d| valid?(d)}
          dirs.sort!{|a, b| Gem::Version.new(b) <=> Gem::Version.new(a)}
          dirs
        end

        # Get the version number of the installed core documentation that is

        # the closest match for the current Ruby version.

        #

        # @return [String] The closest match

        def best_match
          avail = versions
          cur = Gem::Version.new(RUBY_VERSION)
          avail.each do |v|
            return v if Gem::Version.new(v) <= cur
          end
          avail.last
        end

        # Get a list of core documentation versions that are available for

        # download.

        #

        # @return [Array<String>] The version numbers

        def available
          uri = URI.parse("#{SOURCE}/versions.json")
          response = Net::HTTP.get_response(uri)
          obj = JSON.parse(response.body)
          raise SourceNotAvailableError, "Error connecting to #{SOURCE}" unless obj['status'] == 'ok'
          obj['cores']
        end

        # Get the version number of core documentation available for download

        # that is the closest match for the current Ruby version.

        #

        # @param current [String] The version to compare

        # @return [String] The version number of the best match

        def best_download current = RUBY_VERSION
          rv = Gem::Version.new(current)
          found = available
          found.each do |ver|
            return ver if Gem::Version.new(ver) <= rv
          end
          found.last
        end

        # Get the path to a yardoc file for Ruby core documentation.

        #

        # @param ver [String] The version number (best match is default)

        # @return [String] The path to the yardoc

        def yardoc_file(ver = best_match)
          raise ArgumentError, "Invalid core yardoc version #{ver}" unless valid?(ver)
          File.join(cache_dir, ver, 'yardoc')
        end

        # Get the path to a yardoc file for Ruby stdlib documentation.

        #

        # @param ver [String] The version number (best match is default)

        # @return [String] The path to the yardoc

        def yardoc_stdlib_file(ver = best_match)
          raise ArgumentError, "Invalid core yardoc version #{ver}" unless valid?(ver)
          File.join(cache_dir, ver, 'yardoc-stdlib')
        end

        # Download the specified version of core documentation.

        #

        # @param version [String]

        # @return [void]

        def download version
          FileUtils.mkdir_p cache_dir
          uri = URI.parse("#{SOURCE}/#{version}.tar.gz")
          # @type [Net::HTTPResponse]

          response = Net::HTTP.get_response(uri)
          if response.code == '404'
            raise ArgumentError, "Version #{version} is not available from #{SOURCE}"
          else
            zipfile = File.join(cache_dir, "#{version}.tar.gz")
            File.binwrite zipfile, response.body
            install_archive zipfile
          end
        end

        # Reset the core documentation cache to the minimum requirement.

        #

        # @return [void]

        def clear
          FileUtils.rm_rf cache_dir, secure: true
          require_minimum
        end

        private

        # Extract the specified archive to the core cache directory.

        #

        # @param filename [String]

        # @return [void]

        def install_archive filename
          tar_extract = Gem::Package::TarReader.new(Zlib::GzipReader.open(filename))
          tar_extract.rewind
          tar_extract.each do |entry|
            if entry.directory?
              FileUtils.mkdir_p File.join(cache_dir, entry.full_name)
            else
              FileUtils.mkdir_p File.join(cache_dir, File.dirname(entry.full_name))
              File.open(File.join(cache_dir, entry.full_name), 'wb') do |f|
                f << entry.read
              end
            end
          end
          tar_extract.close
        end
      end
    end
  end
end