Class: Rails::Hyperdrive::GemManifest

Inherits:
Object
  • Object
show all
Defined in:
lib/rails/hyperdrive/gem_manifest.rb

Overview

Everything the installer reads from a companion gem: where each artifact kind lives, and which artifacts a given bundle gets. Fail-open at every level — malformed input warns and resolves to an ungated install at the default roots; an artifact is never skipped for unreadable config, and nothing raises.

Defined Under Namespace

Classes: Gate, GemKey, TargetSpec

Constant Summary collapse

FILE_NAME =
"hyperdrive.yml"
METADATA_KEY =
"hyperdrive_manifest"
SKILL_TEMPLATES_DIR_KEY =
"skill_templates_dir".freeze
DIR_KEYS =
(InstallLayout.dir_keys + [SKILL_TEMPLATES_DIR_KEY]).freeze
UNGATED =
["*"].freeze
MATCH_MODES =
%w[any all].freeze
GEM_FORMS =
"a gem, a comma-separated list, a YAML list of names and name: requirement " \
"pairs, or an any:/all: map (gems: is an alias)".freeze
VERSIONS_REMOVED =
"versions: is no longer supported; version constraints ignored — " \
"put the requirement on the gem: member, e.g. `- name: \">= 1.0\"`".freeze

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(spec, warnings:) ⇒ GemManifest

Returns a new instance of GemManifest.



218
219
220
221
222
223
224
225
226
227
228
229
230
231
# File 'lib/rails/hyperdrive/gem_manifest.rb', line 218

def initialize(spec, warnings:)
  @spec = spec
  @warnings = warnings
  root = load_root
  @sections = {}
  @prefixes = {}
  InstallLayout.content_kinds.each do |kind|
    entries, settings = section(root, kind)
    @sections[kind.type] = entries
    @prefixes[kind.type] = read_name_prefix(kind, settings)
  end
  @dirs = DIR_KEYS.each_with_object({}) { |key, h| h[key] = resolve_dir(root, key) }
  @default_spec, @default_fence = defaults(root)
end

Class Method Details

.gem_key(map) ⇒ Object

gems: and gem: are exact aliases. A map carrying both is a stylistic slip, not malformed input: picking a winner keeps the gate intact where failing open would silently widen it.



42
43
44
45
46
47
48
49
50
# File 'lib/rails/hyperdrive/gem_manifest.rb', line 42

def gem_key(map)
  both = map.key?("gem") && map.key?("gems")
  GemKey.new(
    value: map.key?("gems") ? map["gems"] : map["gem"],
    present: map.key?("gem") || map.key?("gems"),
    conflict: both,
    warning: ("gem: and gems: are aliases; reading gems: and ignoring gem:" if both)
  )
end

.load(spec, warnings: []) ⇒ Object



35
36
37
# File 'lib/rails/hyperdrive/gem_manifest.rb', line 35

def load(spec, warnings: [])
  new(spec, warnings: warnings)
end

.malformed_fence?(value) ⇒ Boolean

The fence names one requirement against one version, so a per-gem requirement map is meaningless here.

Returns:

  • (Boolean)


93
94
95
96
# File 'lib/rails/hyperdrive/gem_manifest.rb', line 93

def malformed_fence?(value)
  return true if value.is_a?(Hash)
  malformed_requirements?(value)
end

.malformed_requirements?(versions) ⇒ Boolean

A version requirement is optional everywhere; nil means unconstrained.

Returns:

  • (Boolean)


79
80
81
82
83
84
85
86
87
88
89
# File 'lib/rails/hyperdrive/gem_manifest.rb', line 79

def malformed_requirements?(versions)
  requirements = versions.is_a?(Hash) ? versions.values : [versions]
  requirements.compact.any? do |req|
    begin
      Gem::Requirement.new(*requirement_parts(req))
      false
    rescue ArgumentError
      true
    end
  end
end

.manifest_relpath(spec) ⇒ Object

".." segments are rejected (silent fallback to the conventional path) to prevent escaping the gem root.



54
55
56
57
58
59
60
# File 'lib/rails/hyperdrive/gem_manifest.rb', line 54

def manifest_relpath(spec)
  return FILE_NAME unless spec.respond_to?(:metadata)
  raw = spec. && spec.[METADATA_KEY]
  return FILE_NAME if raw.nil? || raw.to_s.strip.empty?
  return FILE_NAME if raw.to_s.split(%r{[/\\]}).include?("..")
  raw.to_s
end

.opt_in?(spec) ⇒ Boolean

The metadata key counts even when its value is unusable — declaring it at all signals companion intent.

Returns:

  • (Boolean)


64
65
66
67
68
69
70
# File 'lib/rails/hyperdrive/gem_manifest.rb', line 64

def opt_in?(spec)
  return true if File.file?(File.join(spec.full_gem_path, FILE_NAME))
  return false unless spec.respond_to?(:metadata)

  raw = spec. && spec.[METADATA_KEY]
  !raw.to_s.strip.empty?
end

.parse_targets(raw) ⇒ Object

nil signals malformed; the caller decides how to fail open.



73
74
75
76
# File 'lib/rails/hyperdrive/gem_manifest.rb', line 73

def parse_targets(raw)
  return parse_target_map(raw) if raw.is_a?(Hash)
  parse_target_names(raw)
end

.read_dir(root, key) ⇒ Object

Returns [relative directory or nil, failure reason or nil]. Absent or blank is no failure — the caller uses its own default root. ".." segments are rejected to prevent escaping the gem root.



105
106
107
108
109
110
111
112
113
# File 'lib/rails/hyperdrive/gem_manifest.rb', line 105

def read_dir(root, key)
  raw = root[key]
  return [nil, nil] if raw.nil?
  return [nil, "#{key}: must be a directory path relative to the gem root"] unless raw.is_a?(String)
  return [nil, nil] if raw.strip.empty?
  return [nil, "#{key}: must not contain '..' segments"] if raw.split(%r{[/\\]}).include?("..")

  [raw, nil]
end

.read_root(path) ⇒ Object

Returns [root map or nil, failure reason or nil]. An absent or empty file is a valid empty manifest; the caller phrases the failure and decides whether to fall open on it.



118
119
120
121
122
123
124
125
126
127
128
129
130
# File 'lib/rails/hyperdrive/gem_manifest.rb', line 118

def read_root(path)
  return [{}, nil] unless File.file?(path)

  data = YAML.safe_load(File.read(path), permitted_classes: [Symbol])
  return [{}, nil] if data.nil?
  return [data.transform_keys(&:to_s), nil] if data.is_a?(Hash)

  [nil, "root must be a YAML map"]
rescue Psych::SyntaxError => e
  [nil, "malformed YAML (#{e.message})"]
rescue StandardError => e
  [nil, "unreadable (#{e.message})"]
end

.requirement_parts(req) ⇒ Object



98
99
100
# File 'lib/rails/hyperdrive/gem_manifest.rb', line 98

def requirement_parts(req)
  Array(req).flat_map { |s| s.is_a?(String) ? s.split(",").map(&:strip) : s }
end

Instance Method Details

#dir(key) ⇒ Object

nil means the caller's default root.



250
251
252
# File 'lib/rails/hyperdrive/gem_manifest.rb', line 250

def dir(key)
  @dirs[key.to_s]
end

#gate(type, key) ⇒ Object



233
234
235
236
237
# File 'lib/rails/hyperdrive/gem_manifest.rb', line 233

def gate(type, key)
  entries = @sections[type] || {}
  return default_gate unless entries.key?(key)
  entry_gate(entries[key], key, with_conditional: InstallLayout.kind(type).dir_shaped?)
end

#name_prefix(type) ⇒ Object

The section-level scalar a kind prepends to every artifact name it discovers; nil when absent or unusable.



245
246
247
# File 'lib/rails/hyperdrive/gem_manifest.rb', line 245

def name_prefix(type)
  @prefixes[type]
end

#section_keys(type) ⇒ Object



239
240
241
# File 'lib/rails/hyperdrive/gem_manifest.rb', line 239

def section_keys(type)
  (@sections[type] || {}).keys
end

#skill_templates_dirObject



258
259
260
# File 'lib/rails/hyperdrive/gem_manifest.rb', line 258

def skill_templates_dir
  dir(SKILL_TEMPLATES_DIR_KEY)
end

#skills_dirObject



254
255
256
# File 'lib/rails/hyperdrive/gem_manifest.rb', line 254

def skills_dir
  dir(InstallLayout.kind(:skill).dir_key)
end