Skip to content

Instantly share code, notes, and snippets.

@Kerrick
Created December 24, 2025 01:13
Show Gist options
  • Select an option

  • Save Kerrick/0499439695e8f0ced21fb62a562ab87d to your computer and use it in GitHub Desktop.

Select an option

Save Kerrick/0499439695e8f0ced21fb62a562ab87d to your computer and use it in GitHub Desktop.
# frozen_string_literal: true
# SPDX-FileCopyrightText: 2025 Kerrick Long <[email protected]>
# SPDX-License-Identifier: MIT-0
# This rake task includes creative contributions from Gemini 3 Pro.
# https://declare-ai.org/1.0.0/creative.html
require "rubygems"
# 1. The Value Object
# It is not a Calculator; it is the SemVer itself.
# It responds to 'next', returning a new SemVer.
class SemVer
SEGMENTS = [:major, :minor, :patch].freeze
def initialize(version_string)
@gem_version = Gem::Version.new(version_string)
end
def next(segment)
index = SEGMENTS.index(segment)
raise ArgumentError, "Invalid segment: #{segment}" unless index
raw_segments = @gem_version.segments.fill(0, 3).first(3)
raw_segments[index] += 1
raw_segments.fill(0, (index + 1)..2)
SemVer.new(raw_segments.join("."))
end
def to_s
@gem_version.to_s
end
end
# 2. The Manifest
# Not an "Updater". This object REPRESENTS the file on the disk.
# We don't "run" it. We write to it.
class Manifest < Data.define(:path, :pattern, :template)
def read
File.read(path)
end
# The verb is 'write', specific to the object (a file).
def write(version)
return unless File.exist?(path)
new_content = read.gsub(pattern, template % version)
File.write(path, new_content)
puts "Wrote #{version} to #{path}"
end
end
# 3. The Lockfile
# This represents the binary/complex state of the Cargo.lock.
# It doesn't "update"; it "refreshes" or "synchronizes".
class Lockfile
PATH = "ext/ratatui_ruby/Cargo.lock"
DIR = "ext/ratatui_ruby"
def exists?
File.exist?(PATH)
end
def refresh
return unless exists?
Dir.chdir(DIR) do
# "system" is the verb here.
system("cargo update -p ratatui_ruby --offline")
end
puts "Refreshed #{PATH}"
end
end
# 4. The Aggregate Root: Project
# The Project is the noun that holds the state.
# It allows us to issue commands ("bump") or ask queries ("version").
class Project
def initialize(manifests:, lockfile:)
@manifests = manifests
@lockfile = lockfile
end
# Query: Noun
def version
# The first manifest is the source of truth
source = @manifests.first.read
match = source.match(@manifests.first.pattern)
raise "Version missing in manifest" unless match
SemVer.new(match[1])
end
# Command: Verb
def bump(segment)
target = version.next(segment)
puts "Bumping #{segment}: #{version} -> #{target}"
# We iterate over the nouns (manifests) and issue the verb (write)
@manifests.each { |manifest| manifest.write(target) }
# We command the lockfile to refresh
@lockfile.refresh
end
end
# --- Rake Task Integration ---
namespace :bump do
# Composition: The Project is made of these parts.
project = Project.new(
manifests: [
Manifest.new(
path: "lib/ratatui_ruby/version.rb",
pattern: /VERSION = "(.*)"/,
template: 'VERSION = "%s"'
),
Manifest.new(
path: "ext/ratatui_ruby/Cargo.toml",
pattern: /^version = "(.*)"/,
template: 'version = "%s"'
)
],
lockfile: Lockfile.new
)
SemVer::SEGMENTS.each do |segment|
desc "Bump #{segment} version"
task segment do
project.bump(segment)
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment