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