Model seed photos and assets as Data classes
Assisted-by: Claude Opus 4.8 via Claude Code
change ssnlxyrwvkwwqnsloxttrwqkxmxyysuk
commit 5d2c4288fd9efc13601e2563811a255e02a64d9a
author Alpha Chen <alpha@kejadlen.dev>
date
parent wywotopw
diff --git a/lib/seeds.rb b/lib/seeds.rb
index 9f816f7..c2fa000 100644
--- a/lib/seeds.rb
+++ b/lib/seeds.rb
@@ -13,44 +13,55 @@ module Domus
   module Seeds
     USER_AGENT = "domus-seed/1.0 (https://github.com/kejadlen/domus)"
 
-    # CC0 cat photos keyed by a short name. CC0 needs no attribution, but the
-    # Wikimedia Commons file pages document the license and provenance:
+    # A seed photo. +key+ names the cache file; +url+ is the source download.
+    Photo = Data.define(
+      :key, #: Symbol
+      :url, #: String
+    )
+
+    # An asset to seed, with zero or more attached photos.
+    Asset = Data.define(
+      :name, #: String
+      :description, #: String
+      :photos, #: Array[Photo]
+    )
+
+    # CC0 cat photos. CC0 needs no attribution, but the Wikimedia Commons file
+    # pages document the license and provenance:
     #   https://creativecommons.org/publicdomain/zero/1.0/
-    #   grass:  commons.wikimedia.org/wiki/File:Domestic_shorthair_cat_portrait_in_grass.jpg
-    #   tabby:  commons.wikimedia.org/wiki/File:Tabby_cat_with_blue_eyes-3336579.jpg
-    #   paw:    commons.wikimedia.org/wiki/File:Domestic_cat_paw.jpg
-    #   kitten: commons.wikimedia.org/wiki/File:A_mother_cat_of_Meitei_domestic_cat_breed_(Meitei_house_cat_variety)_suckling_her_only_little_newborn_baby_kitten_05.jpg
-    PHOTOS = {
-      grass: "https://upload.wikimedia.org/wikipedia/commons/thumb/3/3c/Domestic_shorthair_cat_portrait_in_grass.jpg/960px-Domestic_shorthair_cat_portrait_in_grass.jpg",
-      tabby: "https://upload.wikimedia.org/wikipedia/commons/thumb/c/c7/Tabby_cat_with_blue_eyes-3336579.jpg/960px-Tabby_cat_with_blue_eyes-3336579.jpg",
-      paw: "https://upload.wikimedia.org/wikipedia/commons/thumb/c/cc/Domestic_cat_paw.jpg/960px-Domestic_cat_paw.jpg",
-      kitten: "https://upload.wikimedia.org/wikipedia/commons/thumb/6/62/A_mother_cat_of_Meitei_domestic_cat_breed_%28Meitei_house_cat_variety%29_suckling_her_only_little_newborn_baby_kitten_05.jpg/960px-A_mother_cat_of_Meitei_domestic_cat_breed_%28Meitei_house_cat_variety%29_suckling_her_only_little_newborn_baby_kitten_05.jpg",
-    } #: Hash[Symbol, String]
+    #   commons.wikimedia.org/wiki/File:Domestic_shorthair_cat_portrait_in_grass.jpg
+    #   commons.wikimedia.org/wiki/File:Tabby_cat_with_blue_eyes-3336579.jpg
+    #   commons.wikimedia.org/wiki/File:Domestic_cat_paw.jpg
+    #   commons.wikimedia.org/wiki/File:A_mother_cat_of_Meitei_domestic_cat_breed_(Meitei_house_cat_variety)_suckling_her_only_little_newborn_baby_kitten_05.jpg
+    GRASS = Photo.new(:grass, "https://upload.wikimedia.org/wikipedia/commons/thumb/3/3c/Domestic_shorthair_cat_portrait_in_grass.jpg/960px-Domestic_shorthair_cat_portrait_in_grass.jpg")
+    TABBY = Photo.new(:tabby, "https://upload.wikimedia.org/wikipedia/commons/thumb/c/c7/Tabby_cat_with_blue_eyes-3336579.jpg/960px-Tabby_cat_with_blue_eyes-3336579.jpg")
+    PAW = Photo.new(:paw, "https://upload.wikimedia.org/wikipedia/commons/thumb/c/cc/Domestic_cat_paw.jpg/960px-Domestic_cat_paw.jpg")
+    KITTEN = Photo.new(:kitten, "https://upload.wikimedia.org/wikipedia/commons/thumb/6/62/A_mother_cat_of_Meitei_domestic_cat_breed_%28Meitei_house_cat_variety%29_suckling_her_only_little_newborn_baby_kitten_05.jpg/960px-A_mother_cat_of_Meitei_domestic_cat_breed_%28Meitei_house_cat_variety%29_suckling_her_only_little_newborn_baby_kitten_05.jpg")
 
     # Sample household assets. The drill is intentionally photoless so the
     # asset page's add-photo affordance shows up in dev.
     ASSETS = [
-      {
+      Asset.new(
         name: "Bosch 800 dishwasher",
         description: "Stainless interior, third rack. Replaces the GE that flooded.",
-        photos: %i[grass],
-      },
-      {
+        photos: [GRASS],
+      ),
+      Asset.new(
         name: "LG French-door refrigerator",
         description: "Counter-depth, craft-ice maker. Bought refurbished in 2023.",
-        photos: %i[tabby kitten],
-      },
-      {
+        photos: [TABBY, KITTEN],
+      ),
+      Asset.new(
         name: "Weber Genesis grill",
         description: "Three-burner propane. Cover lives in the shed over winter.",
-        photos: %i[paw],
-      },
-      {
+        photos: [PAW],
+      ),
+      Asset.new(
         name: "Ryobi cordless drill",
         description: "18V ONE+. Two batteries, charger in the garage drawer.",
         photos: [],
-      },
-    ] #: Array[Hash[Symbol, untyped]]
+      ),
+    ] #: Array[Asset]
 
     # Seeds the database when it's empty, returning true when it inserted data
     # and false when assets already exist. The empty check keeps repeated dev
@@ -61,18 +72,18 @@ module Domus
       db = app.db
       return false unless db[:assets].empty?
 
-      keys = ASSETS.flat_map { |spec| spec[:photos] }.uniq #: Array[Symbol]
-      sources = keys.to_h { |key| [key, fetch(key)] } #: Hash[Symbol, Pathname]
+      photos = ASSETS.flat_map(&:photos).uniq #: Array[Photo]
+      sources = photos.to_h { |photo| [photo, fetch(photo)] } #: Hash[Photo, Pathname]
 
       db.transaction do
         now = Time.now
-        ASSETS.each do |spec|
-          asset_id = db[:assets].insert(name: spec[:name], description: spec[:description], created_at: now)
-          spec[:photos].each do |key|
+        ASSETS.each do |asset|
+          asset_id = db[:assets].insert(name: asset.name, description: asset.description, created_at: now)
+          asset.photos.each do |photo|
             file_id = db[:files].insert(extension: ".jpg", created_at: now)
             dest = app.file_path(id: file_id, extension: ".jpg")
             FileUtils.mkdir_p(dest.dirname)
-            FileUtils.cp(sources.fetch(key), dest)
+            FileUtils.cp(sources.fetch(photo), dest)
             db[:asset_attachments].insert(asset_id:, file_id:, created_at: now)
           end
         end
@@ -81,13 +92,13 @@ module Domus
     end
 
     # Returns the cached path to a seed photo, downloading it on first use.
-    # : (Symbol) -> Pathname
-    def self.fetch(key)
-      path = cache_dir / "#{key}.jpg"
+    # : (Photo) -> Pathname
+    def self.fetch(photo)
+      path = cache_dir / "#{photo.key}.jpg"
       return path if path.exist?
 
       cache_dir.mkpath
-      data = URI.parse(PHOTOS.fetch(key)).open("User-Agent" => USER_AGENT, &:read) #: String
+      data = URI.parse(photo.url).open("User-Agent" => USER_AGENT, &:read) #: String
       path.binwrite(data)
       path
     end
diff --git a/sig/generated/seeds.rbs b/sig/generated/seeds.rbs
index a399e60..f83014c 100644
--- a/sig/generated/seeds.rbs
+++ b/sig/generated/seeds.rbs
@@ -9,18 +9,54 @@ module Domus
   module Seeds
     USER_AGENT: ::String
 
-    # CC0 cat photos keyed by a short name. CC0 needs no attribution, but the
-    # Wikimedia Commons file pages document the license and provenance:
+    # A seed photo. +key+ names the cache file; +url+ is the source download.
+    class Photo < Data
+      attr_reader key(): Symbol
+
+      attr_reader url(): String
+
+      def self.new: (Symbol key, String url) -> instance
+                  | (key: Symbol, url: String) -> instance
+
+      def self.members: () -> [ :key, :url ]
+
+      def members: () -> [ :key, :url ]
+    end
+
+    # An asset to seed, with zero or more attached photos.
+    class Asset < Data
+      attr_reader name(): String
+
+      attr_reader description(): String
+
+      attr_reader photos(): Array[Photo]
+
+      def self.new: (String name, String description, Array[Photo] photos) -> instance
+                  | (name: String, description: String, photos: Array[Photo]) -> instance
+
+      def self.members: () -> [ :name, :description, :photos ]
+
+      def members: () -> [ :name, :description, :photos ]
+    end
+
+    # CC0 cat photos. CC0 needs no attribution, but the Wikimedia Commons file
+    # pages document the license and provenance:
     #   https://creativecommons.org/publicdomain/zero/1.0/
-    #   grass:  commons.wikimedia.org/wiki/File:Domestic_shorthair_cat_portrait_in_grass.jpg
-    #   tabby:  commons.wikimedia.org/wiki/File:Tabby_cat_with_blue_eyes-3336579.jpg
-    #   paw:    commons.wikimedia.org/wiki/File:Domestic_cat_paw.jpg
-    #   kitten: commons.wikimedia.org/wiki/File:A_mother_cat_of_Meitei_domestic_cat_breed_(Meitei_house_cat_variety)_suckling_her_only_little_newborn_baby_kitten_05.jpg
-    PHOTOS: Hash[Symbol, String]
+    #   commons.wikimedia.org/wiki/File:Domestic_shorthair_cat_portrait_in_grass.jpg
+    #   commons.wikimedia.org/wiki/File:Tabby_cat_with_blue_eyes-3336579.jpg
+    #   commons.wikimedia.org/wiki/File:Domestic_cat_paw.jpg
+    #   commons.wikimedia.org/wiki/File:A_mother_cat_of_Meitei_domestic_cat_breed_(Meitei_house_cat_variety)_suckling_her_only_little_newborn_baby_kitten_05.jpg
+    GRASS: untyped
+
+    TABBY: untyped
+
+    PAW: untyped
+
+    KITTEN: untyped
 
     # Sample household assets. The drill is intentionally photoless so the
     # asset page's add-photo affordance shows up in dev.
-    ASSETS: Array[Hash[Symbol, untyped]]
+    ASSETS: Array[Asset]
 
     # Seeds the database when it's empty, returning true when it inserted data
     # and false when assets already exist. The empty check keeps repeated dev
@@ -30,8 +66,8 @@ module Domus
     def self.call: (untyped app) -> untyped
 
     # Returns the cached path to a seed photo, downloading it on first use.
-    # : (Symbol) -> Pathname
-    def self.fetch: (untyped key) -> untyped
+    # : (Photo) -> Pathname
+    def self.fetch: (untyped photo) -> untyped
 
     # The XDG cache directory for downloaded seed photos.
     # : () -> Pathname