lib/licensed/dependency.rb in licensed-1.5.2 vs lib/licensed/dependency.rb in licensed-2.0.0

- old
+ new

@@ -1,91 +1,149 @@ # frozen_string_literal: true require "licensee" module Licensed - class Dependency < License - LEGAL_FILES = /\A(AUTHORS|NOTICE|LEGAL)(?:\..*)?\z/i + class Dependency < Licensee::Projects::FSProject + LEGAL_FILES_PATTERN = /(AUTHORS|NOTICE|LEGAL)(?:\..*)?\z/i - attr_reader :path - attr_reader :search_root attr_reader :name + attr_reader :version + attr_reader :errors - def initialize(path, metadata = {}) - @search_root = metadata.delete("search_root") - @name = metadata.delete("path") || metadata["name"] - super metadata + # Create a new project dependency + # + # name - unique dependency name + # version - dependency version + # path - absolute file path to the dependency, to find license contents + # search_root - (optional) the root location to search for dependency license contents + # metadata - (optional) additional dependency data to cache + # errors - (optional) errors encountered when evaluating dependency + # + # Returns a new dependency object. Dependency metadata and license contents + # are available if no errors are set on the dependency. + def initialize(name:, version:, path:, search_root: nil, metadata: {}, errors: []) + # check the path for default errors if no other errors + # were found when loading the dependency + if errors.empty? + path_error = path_error(path, search_root) + errors.push(path_error) if path_error + end - self.path = path - end + @name = name + @version = version + @metadata = metadata + @errors = errors - # Returns a Licensee::Projects::FSProject for the dependency path - def project - @project ||= Licensee::Projects::FSProject.new(path, search_root: search_root, detect_packages: true, detect_readme: true) - end + # if there are any errors, don't evaluate any dependency contents + return if errors.any? - # Sets the path to source dependency license information - def path=(path) # enforcing absolute paths makes life much easier when determining # an absolute file path in #notices - unless Pathname.new(path).absolute? - raise "Dependency path #{path} must be absolute" + if !Pathname.new(path).absolute? + # this is an internal error related to source implementation and + # should be raised, not stored to be handled by reporters + raise ArgumentError, "dependency path #{path} must be absolute" end - @path = path - reset_license! + super(path, search_root: search_root, detect_readme: true, detect_packages: true) end - # Detects license information and sets it on this dependency object. - # After calling `detect_license!``, the license is set at - # `dependency["license"]` and legal text is set to `dependency.text` - def detect_license! - self["license"] = license_key - self.text = [license_text, *notices].join("\n" + TEXT_SEPARATOR + "\n").rstrip + # Returns true if the dependency has any errors, false otherwise + def errors? + errors.any? end - # Extract legal notices from the dependency source - def notices - local_files.uniq # unique local file paths - .sort # sorted by the path - .map { |f| File.read(f) } # read the file contents - .map(&:rstrip) # strip whitespace - .select { |t| t.length > 0 } # files with content only + # Returns a record for this dependency including metadata and legal contents + def record + return nil if errors? + @record ||= DependencyRecord.new( + metadata: license_metadata, + licenses: license_contents, + notices: notice_contents + ) end - # Returns an array of file paths used to locate legal notices - def local_files - return [] unless Dir.exist?(path) + # Returns a string representing the dependencys license + def license_key + return "none" if errors? || !license + license.key + end - Dir.foreach(path).map do |file| - next unless file.match(LEGAL_FILES) + # Returns the license text content from all matched sources + # except the package file, which doesn't contain license text. + def license_contents + return [] if errors? + matched_files.reject { |f| f == package_file } + .group_by(&:content) + .map { |content, files| { "sources" => content_sources(files), "text" => content } } + end - file_path = File.join(path, file) - next unless File.file?(file_path) + # Returns legal notices found at the dependency path + def notice_contents + return [] if errors? + notice_files.sort # sorted by the path + .map { |file| { "sources" => content_sources(file), "text" => File.read(file).rstrip } } + .select { |text| text.length > 0 } # files with content only + end - file_path - end.compact + # Returns an array of file paths used to locate legal notices + def notice_files + return [] if errors? + + Dir.glob(dir_path.join("*")) + .grep(LEGAL_FILES_PATTERN) + .select { |path| File.file?(path) } end private - # Resets all local project and license information - def reset_license! - @project = nil - self.delete("license") - self.text = nil + def path_error(path, search_root) + return "dependency path not found" if path.to_s.empty? + return if File.exist?(path) + return if search_root && File.exist?(search_root) + + # if the given path doesn't exist + # AND a search root isn't given, or the search root doesn't exist + # then set an error that the expected dependency path doesn't exist + "expected dependency path #{path} does not exist" end - # Regardless of the license detected, try to pull the license content - # from the local LICENSE-type files, remote LICENSE, or the README, in that order - def license_text - content_files = Array(project.license_files) - content_files << project.readme_file if content_files.empty? && project.readme_file - content_files.map(&:content).join("\n#{LICENSE_SEPARATOR}\n") + # Returns the sources for a group of license or notice file contents + # + # Sources are returned as a single string with sources separated by ", " + def content_sources(files) + paths = Array(files).map do |file| + path = if file.is_a?(Licensee::ProjectFiles::ProjectFile) + dir_path.join(file[:dir], file[:name]) + else + Pathname.new(file).expand_path(dir_path) + end + + if path.fnmatch?(dir_path.join("**").to_path) + # files under the dependency path return the relative path to the file + path.relative_path_from(dir_path).to_path + else + # otherwise return the source_path as the immediate parent folder name + # joined with the file name + path.dirname.basename.join(path.basename).to_path + end + end + + paths.join(", ") end - # Returns a string representing the project's license - def license_key - return "none" unless project.license - project.license.key + # Returns the metadata that represents this dependency. This metadata + # is written to YAML in the dependencys cached text file + def license_metadata + { + # can be overriden by values in @metadata + "name" => name, + "version" => version + }.merge( + @metadata + ).merge({ + # overrides all other values + "license" => license_key + }) end end end