Add RBS type signatures to all lib/ source files
Adds # rbs_inline: enabled pragmas and type annotations to all Ruby
source files in lib/. Generates corresponding RBS signatures via
rbs-inline and adds a sig/shim.rbs for third-party gem types (Sequel,
Roda, Phlex) that can't be fetched via rbs collection in this
environment. Expands Steepfile to check all typed files. Excludes
lib/web.rb from Steep checking because Roda's route-block self-rewriting
is incompatible with static analysis.
Assisted-by: Owl Alpha via pi
diff --git a/Steepfile b/Steepfile
index ab13d20..8e7d2a2 100644
--- a/Steepfile
+++ b/Steepfile
@@ -1,4 +1,11 @@
target :lib do
signature "sig"
check "lib/config.rb"
+ check "lib/app.rb"
+ check "lib/models.rb"
+ check "lib/relative_time.rb"
+ check "lib/views/layout.rb"
+ check "lib/views/home.rb"
+ check "lib/views/capture_form.rb"
+ check "lib/views/icons.rb"
end
diff --git a/lib/app.rb b/lib/app.rb
index f72658a..389ccaa 100644
--- a/lib/app.rb
+++ b/lib/app.rb
@@ -1,3 +1,4 @@
+# rbs_inline: enabled
# frozen_string_literal: true
require "sequel"
@@ -7,11 +8,13 @@ module Domus
class App
attr_reader :config, :db
+ # : (Config) -> void
def initialize(config = Config.env)
@config = config
@db = Sequel.sqlite(config.database_url)
end
+ # : (Hash[Symbol, untyped]) -> Pathname
def file_path(record)
config.storage_path / "files" / "#{record[:id]}#{record[:extension]}"
end
diff --git a/lib/models.rb b/lib/models.rb
index dff4000..dd524ca 100644
--- a/lib/models.rb
+++ b/lib/models.rb
@@ -1,3 +1,4 @@
+# rbs_inline: enabled
# frozen_string_literal: true
module Domus
diff --git a/lib/relative_time.rb b/lib/relative_time.rb
index e169732..a7ac6d1 100644
--- a/lib/relative_time.rb
+++ b/lib/relative_time.rb
@@ -1,3 +1,4 @@
+# rbs_inline: enabled
# frozen_string_literal: true
module Domus
@@ -6,6 +7,7 @@ module Domus
module RelativeTime
module_function
+ # : (Time at, ?now: Time) -> String
def format(at, now: Time.now)
return "" unless at
diff --git a/lib/views/capture_form.rb b/lib/views/capture_form.rb
index 6cf14c3..e570b19 100644
--- a/lib/views/capture_form.rb
+++ b/lib/views/capture_form.rb
@@ -1,3 +1,4 @@
+# rbs_inline: enabled
# frozen_string_literal: true
require "phlex"
diff --git a/lib/views/home.rb b/lib/views/home.rb
index c1278a6..5bd6252 100644
--- a/lib/views/home.rb
+++ b/lib/views/home.rb
@@ -1,3 +1,4 @@
+# rbs_inline: enabled
# frozen_string_literal: true
require "phlex"
@@ -15,6 +16,7 @@ module Domus
class Home < Phlex::HTML
include Icons
+ # : (assets: Array[Hash[Symbol, untyped]], total: Integer) -> void
def initialize(assets:, total:)
@assets = assets
@total = total
diff --git a/lib/views/icons.rb b/lib/views/icons.rb
index 7c108e3..2f55d5f 100644
--- a/lib/views/icons.rb
+++ b/lib/views/icons.rb
@@ -1,3 +1,4 @@
+# rbs_inline: enabled
# frozen_string_literal: true
require "pathname"
@@ -8,12 +9,13 @@ module Domus
# and can be sized and recoloured with CSS. Mixed into the Phlex views
# that draw icons.
module Icons
- ICONS_DIR = (Pathname(__dir__) / "../../public/icons").expand_path
+ ICONS_DIR = (Pathname(__dir__.to_s) / "../../public/icons").expand_path
ICONS = Hash.new do |cache, name|
cache[name] = (ICONS_DIR / "#{name}.svg").read.freeze
end
+ # : (String name) -> void
def icon(name)
raw safe(ICONS[name])
end
diff --git a/lib/views/layout.rb b/lib/views/layout.rb
index 33d19c9..e423156 100644
--- a/lib/views/layout.rb
+++ b/lib/views/layout.rb
@@ -1,3 +1,4 @@
+# rbs_inline: enabled
# frozen_string_literal: true
require "phlex"
@@ -10,6 +11,7 @@ module Domus
# `scripts` are emitted (deferred) before Alpine so page scripts that
# register Alpine components — e.g. /capture.js defining captureApp() —
# run first. Order matters: deferred scripts execute in document order.
+ # : (?title: String, ?scripts: Array[String]) -> void
def initialize(title: "Domus", scripts: [])
@title = title
@scripts = scripts
diff --git a/lib/web.rb b/lib/web.rb
index b2a580e..120a381 100644
--- a/lib/web.rb
+++ b/lib/web.rb
@@ -1,3 +1,4 @@
+# rbs_inline: enabled
# frozen_string_literal: true
require "roda"
@@ -11,6 +12,7 @@ module Domus
class ClientError < StandardError
attr_reader :status
+ # : (String message, ?status: Integer) -> void
def initialize(message, status: 422)
super(message)
@status = status
@@ -56,11 +58,14 @@ module Domus
private
+ # : () -> App
def app = opts.fetch(:app)
+ # : () -> Sequel::Database
def db = app.db
# Persists an uploaded image, raising ClientError when the upload is
# rejected so the error_handler plugin can render the right status.
+ # : (Hash[String, untyped]) -> void
def save_file(params)
upload = params["file"]
raise ClientError, "Choose a file to upload." unless upload.is_a?(Hash) && upload[:tempfile]
diff --git a/sig/generated/app.rbs b/sig/generated/app.rbs
new file mode 100644
index 0000000..e3d8ddf
--- /dev/null
+++ b/sig/generated/app.rbs
@@ -0,0 +1,15 @@
+# Generated from lib/app.rb with RBS::Inline
+
+module Domus
+ class App
+ attr_reader config: untyped
+
+ attr_reader db: untyped
+
+ # : (Config) -> void
+ def initialize: (?untyped config) -> untyped
+
+ # : (Hash[Symbol, untyped]) -> Pathname
+ def file_path: (untyped record) -> untyped
+ end
+end
diff --git a/sig/generated/models.rbs b/sig/generated/models.rbs
new file mode 100644
index 0000000..627671e
--- /dev/null
+++ b/sig/generated/models.rbs
@@ -0,0 +1,6 @@
+# Generated from lib/models.rb with RBS::Inline
+
+module Domus
+ class Document < Sequel::Model
+ end
+end
diff --git a/sig/generated/relative_time.rbs b/sig/generated/relative_time.rbs
new file mode 100644
index 0000000..ba0d109
--- /dev/null
+++ b/sig/generated/relative_time.rbs
@@ -0,0 +1,10 @@
+# Generated from lib/relative_time.rb with RBS::Inline
+
+module Domus
+ # Formats a timestamp as a compact, archival relative age —
+ # "now", "5m", "3h", "2d", "1w", "4mo", "2y".
+ module RelativeTime
+ # : (Time at, ?now: Time) -> String
+ def self?.format: (untyped at, ?now: untyped) -> untyped
+ end
+end
diff --git a/sig/generated/views/capture_form.rbs b/sig/generated/views/capture_form.rbs
new file mode 100644
index 0000000..a21d78b
--- /dev/null
+++ b/sig/generated/views/capture_form.rbs
@@ -0,0 +1,15 @@
+# Generated from lib/views/capture_form.rb with RBS::Inline
+
+module Domus
+ module Views
+ # The save step of a capture: the hidden camera/file inputs plus the
+ # preview and asset-naming form. Driven by the captureApp() Alpine data,
+ # and shared by both the capture page and the home page so the capture
+ # actions can open the picker in place.
+ class CaptureForm < Phlex::HTML
+ include Icons
+
+ def view_template: () -> untyped
+ end
+ end
+end
diff --git a/sig/generated/views/home.rbs b/sig/generated/views/home.rbs
new file mode 100644
index 0000000..27a3548
--- /dev/null
+++ b/sig/generated/views/home.rbs
@@ -0,0 +1,42 @@
+# Generated from lib/views/home.rb with RBS::Inline
+
+module Domus
+ module Views
+ # The home page — the archive's front door. Capture actions live in a
+ # thumb-reachable dock and open the picker in place: the whole page is one
+ # captureApp() Alpine component, so choosing a file swaps the recent-asset
+ # list for the save form.
+ class Home < Phlex::HTML
+ include Icons
+
+ # : (assets: Array[Hash[Symbol, untyped]], total: Integer) -> void
+ def initialize: (assets: untyped, total: untyped) -> untyped
+
+ def view_template: () -> untyped
+
+ private
+
+ def topbar: () -> untyped
+
+ # A single split button: the primary action opens the picker
+ # captureApp() chooses for the viewport (upload on desktop, camera on
+ # mobile), and the caret opens a drop-up menu with the alternate.
+ # Both labels are rendered and toggled by media query.
+ def capture_control: () -> untyped
+
+ def recent_assets: () -> untyped
+
+ def archive: () -> untyped
+
+ def empty_state: () -> untyped
+
+ # The capture save step, shown once a photo or file has been chosen.
+ def save_panel: () -> untyped
+
+ # The capture front door: a dock fixed to the bottom of the viewport on
+ # every screen, so the primary action stays reachable and out of the
+ # header.
+ def dock: () -> untyped
+ end
+ end
+end
diff --git a/sig/generated/views/icons.rbs b/sig/generated/views/icons.rbs
new file mode 100644
index 0000000..9ed6ec2
--- /dev/null
+++ b/sig/generated/views/icons.rbs
@@ -0,0 +1,20 @@
+# Generated from lib/views/icons.rb with RBS::Inline
+
+module Domus
+ module Views
+ # Inlines the SVG files from public/icons so they inherit currentColor
+ # and can be sized and recoloured with CSS. Mixed into the Phlex views
+ # that draw icons.
+ module Icons
+ ICONS_DIR: untyped
+
+ ICONS: untyped
+
+ # : (String name) -> void
+ def icon: (untyped name) -> untyped
+
+ def raw: (String) -> void
+ def safe: (String) -> String
+ end
+ end
+end
diff --git a/sig/generated/views/layout.rbs b/sig/generated/views/layout.rbs
new file mode 100644
index 0000000..45cef45
--- /dev/null
+++ b/sig/generated/views/layout.rbs
@@ -0,0 +1,17 @@
+# Generated from lib/views/layout.rb with RBS::Inline
+
+module Domus
+ module Views
+ class Layout < Phlex::HTML
+ ALPINE: ::String
+
+ # `scripts` are emitted (deferred) before Alpine so page scripts that
+ # register Alpine components — e.g. /capture.js defining captureApp() —
+ # run first. Order matters: deferred scripts execute in document order.
+ # : (?title: String, ?scripts: Array[String]) -> void
+ def initialize: (?title: untyped, ?scripts: untyped) -> untyped
+
+ def view_template: () ?{ (?) -> untyped } -> untyped
+ end
+ end
+end
diff --git a/sig/generated/web.rbs b/sig/generated/web.rbs
new file mode 100644
index 0000000..288bf4b
--- /dev/null
+++ b/sig/generated/web.rbs
@@ -0,0 +1,31 @@
+# Generated from lib/web.rb with RBS::Inline
+
+module Domus
+ # Raised when a request can't be processed because of client input. The
+ # error_handler plugin renders it with the HTTP status it carries.
+ class ClientError < StandardError
+ attr_reader status: untyped
+
+ # : (String message, ?status: Integer) -> void
+ def initialize: (untyped message, ?status: untyped) -> untyped
+ end
+
+ class Web < Roda
+ IMAGE_EXTENSIONS: untyped
+
+ MAX_UPLOAD_BYTES: untyped
+
+ private
+
+ # : () -> App
+ def app: () -> untyped
+
+ # : () -> Sequel::Database
+ def db: () -> untyped
+
+ # Persists an uploaded image, raising ClientError when the upload is
+ # rejected so the error_handler plugin can render the right status.
+ # : (Hash[String, untyped]) -> void
+ def save_file: (untyped params) -> untyped
+ end
+end
diff --git a/sig/shim.rbs b/sig/shim.rbs
new file mode 100644
index 0000000..55bf4ad
--- /dev/null
+++ b/sig/shim.rbs
@@ -0,0 +1,74 @@
+# Shim declarations for gems without RBS in this environment.
+# Covers only the methods actually used in the Domus codebase.
+
+module Sequel
+ def self.sqlite: (String) -> Sequel::Database
+ def self.desc: (Symbol) -> untyped
+
+ class Database
+ def []: (Symbol) -> Sequel::Dataset
+ def transaction: () { () -> void } -> void
+ end
+
+ class Dataset
+ def order: (*untyped) -> Sequel::Dataset
+ def limit: (Integer) -> Sequel::Dataset
+ def all: () -> Array[Hash[Symbol, untyped]]
+ def count: () -> Integer
+ def insert: (Hash[Symbol, untyped]) -> Integer
+ end
+
+ class Model
+ end
+end
+
+class Roda
+ # Class-level DSL
+ def self.plugin: (Symbol) ?{ (Class) -> void } -> void
+ def self.route: () { (Roda) -> void } -> void
+
+ # Instance-level DSL (used inside route blocks)
+ def db: () -> Sequel::Database
+ def response: () -> untyped
+ def opts: () -> Hash[Symbol, untyped]
+end
+
+module Phlex
+ class HTML
+ def self.new: (**untyped) -> instance
+
+ def view_template: () { () -> void } -> void
+ def render: (untyped) ?{ () -> void } -> void
+ def call: () -> String
+
+ # HTML elements used in views
+ def doctype: () -> void
+ def html: (?lang: String) { () -> void } -> void
+ def head: () { () -> void } -> void
+ def body: () ?{ () -> void } -> void
+ def meta: (?charset: String, ?name: String, ?content: String) -> void
+ def title: () { () -> void } -> void
+ def link: (?rel: String, ?type: String, ?href: String) -> void
+ def script: (?defer: bool, ?src: String) -> void
+ def div: (?class: String, **untyped) { () -> void } -> void
+ def span: (?class: String, **untyped) { () -> void } -> void
+ def a: (?href: String, ?class: String, **untyped) { () -> void } -> void
+ def h3: (**untyped) { () -> void } -> void
+ def p: (?class: String, **untyped) { () -> void } -> void
+ def section: (**untyped) { () -> void } -> void
+ def header: (?class: String, **untyped) { () -> void } -> void
+ def main: (?class: String, **untyped) { () -> void } -> void
+ def form: (?method: String, ?action: String, ?enctype: String, **untyped) { () -> void } -> void
+ def input: (?type: String, ?name: String, ?accept: String, ?capture: String, ?class: String, ?placeholder: String, **untyped) -> void
+ def button: (?type: String, ?class: String, **untyped) { () -> void } -> void
+ def img: (?alt: String, **untyped) -> void
+ def template: (**untyped) { () -> void } -> void
+
+ # Text output
+ def plain: (String) -> void
+
+ # Raw HTML
+ def raw: (String) -> void
+ def safe: (String) -> String
+ end
+end