Class: Kettle::Dev::GemSpecReader

Inherits:
Object
  • Object
show all
Defined in:
lib/kettle/dev/gem_spec_reader.rb

Overview

Unified gemspec reader using RubyGems loader instead of regex parsing.
Returns a Hash with all data used by this project from gemspecs.
Cache within the process to avoid repeated loads.

Constant Summary collapse

DEFAULT_MINIMUM_RUBY =

Default minimum Ruby version to assume when a gemspec doesn’t specify one.

Returns:

  • (Gem::Version)
Gem::Version.new("1.8").freeze

Class Method Summary collapse

Class Method Details

.load(root) ⇒ Hash{Symbol=>Object}

Load gemspec data for the project at root using RubyGems.
The reader is lenient: failures to load or missing fields are handled with defaults and warnings.

Parameters:

  • root (String)

    project root containing a *.gemspec file

  • return (Hash)

    a customizable set of options

Returns:

  • (Hash{Symbol=>Object})

    a Hash of gem metadata used by templating and tasks



41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
# File 'lib/kettle/dev/gem_spec_reader.rb', line 41

def load(root)
  gemspec_path = Dir.glob(File.join(root.to_s, "*.gemspec")).first
  spec = nil
  if gemspec_path && File.file?(gemspec_path)
    begin
      spec = Gem::Specification.load(gemspec_path)
    rescue StandardError => e
      Kettle::Dev.debug_error(e, __method__)
      spec = nil
    end
  end

  gem_name = spec&.name.to_s
  if gem_name.nil? || gem_name.strip.empty?
    # Be lenient here for tasks that can proceed without gem_name (e.g., choosing destination filenames).
    Kernel.warn("kettle-dev: Could not derive gem name. Ensure a valid <name> is set in the gemspec.\n  - Tip: set the gem name in your .gemspec file (spec.name).\n  - Path searched: #{gemspec_path || "(none found)"}")
    gem_name = ""
  end
  # minimum ruby version: derived from spec.required_ruby_version
  # Always an instance of Gem::Version
  min_ruby =
    begin
      # irb(main):004> Gem::Requirement.parse(spec.required_ruby_version)
      # => [">=", Gem::Version.new("2.3.0")]
      requirement = spec&.required_ruby_version
      if requirement
        tuple = Gem::Requirement.parse(requirement)
        tuple[1] # an instance of Gem::Version
      else
        # Default to a minimum of Ruby 1.8
        puts "WARNING: Minimum Ruby not detected"
        DEFAULT_MINIMUM_RUBY
      end
    rescue StandardError => e
      puts "WARNING: Minimum Ruby detection failed:"
      Kettle::Dev.debug_error(e, __method__)
      # Default to a minimum of Ruby 1.8
      DEFAULT_MINIMUM_RUBY
    end

  homepage_val = spec&.homepage.to_s

  # Derive org/repo from homepage or git remote
  forge_info = derive_forge_and_origin_repo(homepage_val)
  forge_org = forge_info[:forge_org]
  gh_repo = forge_info[:origin_repo]
  if forge_org.to_s.empty?
    Kernel.warn("kettle-dev: Could not determine forge org from spec.homepage or git remote.\n  - Ensure gemspec.homepage is set to a GitHub URL or that the git remote 'origin' points to GitHub.\n  - Example homepage: https://github.com/<org>/<repo>\n  - Proceeding with default org: kettle-rb.")
    forge_org = "kettle-rb"
  end

  camel = lambda do |s|
    s.to_s.split(/[_-]/).map { |p| p.gsub(/\b([a-z])/) { Regexp.last_match(1).upcase } }.join
  end
  namespace = gem_name.to_s.split("-").map { |seg| camel.call(seg) }.join("::")
  namespace_shield = namespace.gsub("::", "%3A%3A")
  entrypoint_require = gem_name.to_s.tr("-", "/")
  gem_shield = gem_name.to_s.gsub("-", "--").gsub("_", "__")

  # Funding org (Open Collective handle) detection.
  # Precedence:
  #   1) ENV["FUNDING_ORG"] when set:
  #        - value "false" (any case) disables funding (nil)
  #        - otherwise use the value verbatim
  #   2) OpenCollectiveConfig.handle(required: false)
  # Be lenient: allow nil when not discoverable, with a concise warning.
  begin
    env_funding = ENV["FUNDING_ORG"]
    if env_funding && !env_funding.to_s.strip.empty?
      funding_org = if env_funding.to_s.strip.casecmp("false").zero?
        nil
      else
        env_funding.to_s
      end
    else
      # Preflight: if a YAML exists under the provided root, attempt to read it here so
      # unexpected file IO errors surface within this rescue block (see specs).
      oc_path = OpenCollectiveConfig.yaml_path(root)
      File.read(oc_path) if File.file?(oc_path)

      funding_org = OpenCollectiveConfig.handle(required: false, root: root)
      if funding_org.to_s.strip.empty?
        Kernel.warn("kettle-dev: Could not determine funding org.\n  - Options:\n    * Set ENV['FUNDING_ORG'] to your funding handle, or 'false' to disable.\n    * Or set ENV['OPENCOLLECTIVE_HANDLE'].\n    * Or add .opencollective.yml with: collective: <handle> (or org: <handle>).\n    * Or proceed without funding if not applicable.")
        funding_org = nil
      end
    end
  rescue StandardError => error
    Kettle::Dev.debug_error(error, __method__)
    # In an unexpected exception path, escalate to a domain error to aid callers/specs
    raise Kettle::Dev::Error, "Unable to determine funding org: #{error.message}"
  end

  {
    gemspec_path: gemspec_path,
    gem_name: gem_name,
    min_ruby: min_ruby, # Gem::Version instance
    homepage: homepage_val.to_s,
    gh_org: forge_org, # Might allow divergence from forge_org someday
    forge_org: forge_org,
    funding_org: funding_org,
    gh_repo: gh_repo,
    namespace: namespace,
    namespace_shield: namespace_shield,
    entrypoint_require: entrypoint_require,
    gem_shield: gem_shield,
    # Additional fields sourced from the gemspec for templating carry-over
    authors: Array(spec&.authors).compact.uniq,
    email: Array(spec&.email).compact.uniq,
    summary: spec&.summary.to_s,
    description: spec&.description.to_s,
    licenses: Array(spec&.licenses), # licenses will include any specified as license (singular)
    required_ruby_version: spec&.required_ruby_version, # Gem::Requirement instance
    require_paths: Array(spec&.require_paths),
    bindir: (spec&.bindir || "").to_s,
    executables: Array(spec&.executables),
  }
end