studio-engine 0.10.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 20b0e58f17421ce741452b329000bc657f0dba0304198d7dc1fb59cb448bfc6d
4
- data.tar.gz: d4446a5bad4b0b744e54ad7d4f275cf77455a68f2cf3289d6d9c472ca1f3c548
3
+ metadata.gz: 61bf440c6e7573b35e8d7a680851d2535b37b08690f36c0f8c42402a8a1fcc64
4
+ data.tar.gz: e79e46ab92bcd13bf5655526c199c1d7a1b17610d815dc240a5af7250455b9e1
5
5
  SHA512:
6
- metadata.gz: b15b1f884815e8afbad3d9aa181589230193942970d62457075d08c47521c967a005025560e665b436ceb8a6c78f425688430f122cad9a7f21e8a4432bd4ca22
7
- data.tar.gz: 5a6c6817018309298bb3a8eb686f61d7e93d9718d5b8ce5b5a8e642efc82f99695d6bf0c4f23b9421fa8c65481722e9164934c7cbe47f254e3abb482d7ea42ed
6
+ metadata.gz: 64d04f5527318728c090ad318d9d5f652a12b1e8eadc6f665eccd4a0c4a0aadff6d1c7dfaecdc5c2a438c08b648a34ba78f1869dc8df1b80c6d1ccc9728f457d
7
+ data.tar.gz: bba2fac4175596d6448dcc9611c9c4740f021fcfb467c09986a20d0d506e4c37cc933257ac6e0d23deb262a0fcf98baa3fe3bce104a8985e85562451ddab406f
data/CHANGELOG.md CHANGED
@@ -4,6 +4,45 @@ The format is [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This pro
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ### Added
8
+ - **Model-page protocol (v1)** — a reusable, admin-only per-record inspector,
9
+ drawn into every consuming app via `Studio.routes`:
10
+ `/models/:model/:id` renders one record as pretty JSON plus a copy/paste
11
+ rails-console command to reload it, and `/models/:model/random` bounces to a
12
+ random record of that model. New `Studio::ModelsController` +
13
+ `app/views/studio/models/show.html.erb` (theme-token styled, reuses the
14
+ `components/copy_button` partial). The protocol object `Studio::ModelPage`
15
+ ships an **empty registry** — no model is reachable until a host opts in with
16
+ `Studio::ModelPage.register("release", Release, lookup: :slug)` in an
17
+ initializer (mirrors the admin-models host-registry pattern; the engine never
18
+ constantizes host models by name). Gated with `require_admin`.
19
+
20
+ ## 0.11.0 — 2026-06-24
21
+
22
+ ### Changed
23
+ - **Rails 8.1 support** — widened the gemspec `rails` bound from
24
+ `">= 7.0", "< 8.0"` to `">= 7.2", "< 8.2"`, so consuming apps can migrate to
25
+ Rails 8.1 independently (the engine no longer pins the ecosystem to Rails 7).
26
+ This release is the gate for the McRitchie Rails 8.1 train — `mcritchie-studio`
27
+ and `turf-monster` can bundle Rails 8.1 only once this version is published.
28
+ The lower bound moves to 7.2 (the oldest Rails the apps actually run) since
29
+ Rails 8 requires Ruby >= 3.2 and the ecosystem is already on Ruby 3.3.11.
30
+
31
+ ### Added
32
+ - **`test/dummy` Rails app + `test/integration/engine_rails_8_1_boot_test.rb`** —
33
+ boots the engine inside a real Rails app and exercises its Rails-version-
34
+ sensitive surfaces (the Railtie registration, Zeitwerk autoload wiring, the
35
+ `Studio.routes` DSL, and the `ErrorLog` / `ThemeSetting` / `Sluggable`
36
+ ActiveRecord models). Re-run this against any new Rails line before widening
37
+ the bound again. The engine's full unit suite + this boot test are green
38
+ against Rails 8.1.3 (ActiveSupport/ActiveRecord 8.1, Zeitwerk 2.8, minitest 6).
39
+
40
+ ### Fixed
41
+ - `bin/release-check` now prefers the `ruby@3.3` toolchain (was pinned to the
42
+ retired `ruby@3.1`, which cannot resolve Rails 8.1) and runs the full test
43
+ list, including the new Rails 8.1 boot test and the previously-omitted
44
+ `link_token` / sticky-header / admin-models-table tests.
45
+
7
46
  ## 0.10.0 — 2026-06-24
8
47
 
9
48
  ### Added
data/Gemfile CHANGED
@@ -2,17 +2,19 @@ source "https://rubygems.org"
2
2
 
3
3
  gemspec
4
4
 
5
- # nokogiri 1.18.10 has 3 CVEs (1 HIGH regex-backtracking + 2 MEDIUM
6
- # in XSLT / xmlC14NExecute). Fix is `>= 1.19.3` but nokogiri 1.19.x
7
- # requires Ruby 3.2+. The ecosystem currently runs on Ruby 3.1.7;
8
- # bumping Ruby is a separate effort (across both Rails apps).
9
- #
10
- # **Important**: this gem does NOT declare nokogiri as a runtime
11
- # dependency in its gemspec — nokogiri comes into the consuming
12
- # Rails app via Rails itself. Consumers running Rails on a Ruby
13
- # version that supports nokogiri 1.19.x will resolve it. The
14
- # studio-engine gem's own Gemfile.lock (this repo's dev env) is the
15
- # only place still on 1.18.10.
16
- #
17
- # Tracked in SECURITY-AUDIT-2026-05-17.md. When the ecosystem bumps
18
- # to Ruby 3.2+, add: gem "nokogiri", ">= 1.19.3", group: :development
5
+ # nokogiri: historically pinned to 1.18.10 here while the ecosystem was on
6
+ # Ruby 3.1.7 (nokogiri >= 1.19.x requires Ruby 3.2+). The ecosystem is now on
7
+ # Ruby 3.3.11, so a plain `bundle update` resolves the patched nokogiri
8
+ # (>= 1.19.3, the line that fixed 3 CVEs: 1 HIGH regex-backtracking + 2 MEDIUM
9
+ # XSLT / xmlC14NExecute). This gem still does NOT declare nokogiri as a runtime
10
+ # dependency it arrives via Rails in the consuming app. Tracked in
11
+ # SECURITY-AUDIT-2026-05-17.md.
12
+
13
+ group :development, :test do
14
+ # SQLite drives the test/dummy Rails app's in-memory database (see
15
+ # test/integration/engine_rails_8_1_boot_test.rb), which boots the engine
16
+ # against a real Rails app so we catch Rails-version incompatibilities in the
17
+ # engine's ActiveRecord/Railtie surfaces. Not a runtime dependency — the
18
+ # consuming apps bring their own database adapter. Rails 8.1 needs sqlite3 ~> 2.1.
19
+ gem "sqlite3", ">= 2.1"
20
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Studio
4
+ # Serves the model-page protocol (see Studio::ModelPage). Admin-only: raw record
5
+ # JSON can carry internal fields, so this is an operator/debug surface, not a
6
+ # public one. require_authentication is inherited from the host app's
7
+ # ApplicationController (Studio::ErrorHandling); require_admin gates on top.
8
+ class ModelsController < ApplicationController
9
+ before_action :require_admin
10
+ before_action :load_page
11
+
12
+ # GET /models/:model/:id — one record as JSON + a copy/paste console command.
13
+ def show
14
+ head :not_found unless @page.record
15
+ end
16
+
17
+ # GET /models/:model/random — bounce to a random record's page (fresh sample).
18
+ def random
19
+ record = @page.random_record
20
+ return head(:not_found) unless record
21
+
22
+ redirect_to studio_model_path(@page.model_key, @page.identifier_for(record))
23
+ end
24
+
25
+ private
26
+
27
+ def load_page
28
+ @page = Studio::ModelPage.new(params[:model], params[:id])
29
+ rescue Studio::ModelPage::UnknownModel
30
+ head :not_found
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Studio
6
+ # The model-page protocol (v1).
7
+ #
8
+ # A reusable, per-record inspector. Given a whitelisted model key and a record
9
+ # identifier, it produces the two things a v1 model page renders — the record
10
+ # as pretty-printed JSON and a copy/paste rails-console command that reloads it
11
+ # — plus the lookup a "random sample" jump needs. The controller and view stay
12
+ # thin; enabling a model is a one-line registration.
13
+ #
14
+ # The engine ships an EMPTY registry: no model is reachable until a host app
15
+ # opts in. Each app registers its own models in an initializer, e.g.
16
+ #
17
+ # Studio::ModelPage.register("release", Release, lookup: :slug)
18
+ #
19
+ # This mirrors the admin-models pattern — the host hands the engine live class
20
+ # references; the engine never constantizes host models by name. An unknown key
21
+ # raises UnknownModel, which the controller renders as 404.
22
+ class ModelPage
23
+ class UnknownModel < StandardError; end
24
+
25
+ Entry = Struct.new(:model, :lookup, keyword_init: true)
26
+
27
+ class << self
28
+ # Enable a model. `model` is the live AR class; `lookup` is the column its
29
+ # page URL is keyed on (usually its to_param backing). Returns the key.
30
+ def register(key, model, lookup: :slug)
31
+ registry[key.to_s] = Entry.new(model: model, lookup: lookup.to_sym)
32
+ key.to_s
33
+ end
34
+
35
+ def registered?(key)
36
+ registry.key?(key.to_s)
37
+ end
38
+
39
+ def keys
40
+ registry.keys
41
+ end
42
+
43
+ # Clears the registry — for tests and boot-time re-registration.
44
+ def reset!
45
+ registry.clear
46
+ end
47
+
48
+ def entry(key)
49
+ registry.fetch(key.to_s) { raise UnknownModel, "unknown model: #{key.inspect}" }
50
+ end
51
+
52
+ private
53
+
54
+ def registry
55
+ @registry ||= {}
56
+ end
57
+ end
58
+
59
+ attr_reader :model_key, :identifier
60
+
61
+ # identifier is the record's lookup-key value from the URL (nil for /random,
62
+ # which addresses the model as a whole rather than one record).
63
+ def initialize(model_key, identifier = nil)
64
+ @entry = self.class.entry(model_key)
65
+ @model_key = model_key.to_s
66
+ @identifier = identifier
67
+ end
68
+
69
+ def model
70
+ @entry.model
71
+ end
72
+
73
+ def lookup_key
74
+ @entry.lookup
75
+ end
76
+
77
+ # The record this page addresses (nil when the identifier matches nothing).
78
+ def record
79
+ return @record if defined?(@record)
80
+
81
+ @record = model.find_by(lookup_key => identifier)
82
+ end
83
+
84
+ # A random record of this model, for the "random sample" jump (nil if empty).
85
+ def random_record
86
+ model.order(::Arel.sql("RANDOM()")).first
87
+ end
88
+
89
+ # The copy/paste rails-console command that reloads this record, e.g.
90
+ # Release.find_by(slug: "rel-20260707-a1b2c3")
91
+ def console_command
92
+ %(#{model.name}.find_by(#{lookup_key}: #{identifier.inspect}))
93
+ end
94
+
95
+ # The record serialized as pretty JSON — the page's primary payload.
96
+ def json
97
+ ::JSON.pretty_generate(record.as_json)
98
+ end
99
+
100
+ # The lookup-key value to route to for a given record (used by /random).
101
+ def identifier_for(record)
102
+ record.public_send(lookup_key)
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,32 @@
1
+ <%# Model-page protocol (v1) — a uniform record inspector: the record as pretty
2
+ JSON plus a copy/paste rails-console command to reload it, with a jump to a
3
+ random sample record. Admin-only surface (Studio::ModelsController). The copy
4
+ button reuses the engine components/copy_button partial. %>
5
+ <% model_label = @page.model.model_name.human %>
6
+ <% content_for(:title, "#{model_label} - Model") %>
7
+
8
+ <div class="max-w-7xl mx-auto px-4 py-8 space-y-6">
9
+ <header class="flex flex-col gap-3 md:flex-row md:items-end md:justify-between">
10
+ <div>
11
+ <p class="text-xs text-muted uppercase tracking-wide font-semibold">Model</p>
12
+ <h1 class="text-heading text-2xl font-bold mt-1"><%= model_label %></h1>
13
+ <p class="text-muted text-sm mt-1 font-mono"><%= @page.identifier %></p>
14
+ </div>
15
+ <%= link_to "🎲 Random sample", studio_model_random_path(@page.model_key), class: "btn btn-outline" %>
16
+ </header>
17
+
18
+ <%# The copy/paste line to reload this record in rails console. %>
19
+ <section class="card p-4 space-y-2">
20
+ <p class="text-xs text-muted uppercase tracking-wide font-semibold">Rails console</p>
21
+ <%= render "components/copy_button", text: @page.console_command %>
22
+ </section>
23
+
24
+ <%# The record serialized as pretty JSON — the protocol's primary payload. %>
25
+ <section class="card overflow-hidden">
26
+ <div class="flex items-center justify-between px-4 py-2 bg-surface-alt border-b border-subtle">
27
+ <p class="text-xs text-muted uppercase tracking-wide font-semibold">JSON</p>
28
+ <span class="font-mono text-xs text-muted"><%= @page.model.name %></span>
29
+ </div>
30
+ <pre class="overflow-x-auto px-4 py-3 text-xs font-mono text-heading leading-relaxed bg-inset"><%= @page.json %></pre>
31
+ </section>
32
+ </div>
@@ -1,3 +1,3 @@
1
1
  module Studio
2
- VERSION = "0.10.0"
2
+ VERSION = "0.12.0"
3
3
  end
data/lib/studio.rb CHANGED
@@ -309,6 +309,16 @@ module Studio
309
309
  get "admin/email_images", to: "studio/email_images#index", as: :admin_email_images
310
310
  patch "admin/email_images/:variant", to: "studio/email_images#update", as: :admin_email_image,
311
311
  constraints: { variant: /[a-z_]+/ }
312
+
313
+ # Model-page protocol (v1) — a reusable per-record inspector. Drawn into
314
+ # every consuming app: /models/:model/:id renders one record as pretty JSON
315
+ # plus a copy/paste rails-console command; /models/:model/random bounces to
316
+ # a random record of that model. Admin-only (Studio::ModelsController).
317
+ # Ships an EMPTY registry — a host enables a model in an initializer with
318
+ # `Studio::ModelPage.register("release", Release, lookup: :slug)`. `random`
319
+ # is drawn BEFORE `:id` so it is not captured as a record identifier.
320
+ get "models/:model/random", to: "studio/models#random", as: :studio_model_random
321
+ get "models/:model/:id", to: "studio/models#show", as: :studio_model
312
322
  end
313
323
  end
314
324
  end
@@ -21,7 +21,7 @@ Gem::Specification.new do |spec|
21
21
  spec.files = Dir["lib/**/*", "app/**/*", "config/**/*", "db/**/*", "tailwind/**/*", "Gemfile", "studio-engine.gemspec", "README.md", "CHANGELOG.md", "LICENSE"]
22
22
  spec.require_paths = ["lib"]
23
23
 
24
- spec.add_dependency "rails", ">= 7.0", "< 8.0"
24
+ spec.add_dependency "rails", ">= 7.2", "< 8.2"
25
25
  spec.add_dependency "tailwindcss-rails", "~> 4.5"
26
26
  spec.add_dependency "faker", ">= 2.0", "< 4.0"
27
27
  spec.add_dependency "solid_queue", ">= 1.0", "< 2.0"
metadata CHANGED
@@ -1,13 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: studio-engine
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.10.0
4
+ version: 0.12.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alex McRitchie
8
+ autorequire:
8
9
  bindir: bin
9
10
  cert_chain: []
10
- date: 1980-01-02 00:00:00.000000000 Z
11
+ date: 2026-07-08 00:00:00.000000000 Z
11
12
  dependencies:
12
13
  - !ruby/object:Gem::Dependency
13
14
  name: rails
@@ -15,20 +16,20 @@ dependencies:
15
16
  requirements:
16
17
  - - ">="
17
18
  - !ruby/object:Gem::Version
18
- version: '7.0'
19
+ version: '7.2'
19
20
  - - "<"
20
21
  - !ruby/object:Gem::Version
21
- version: '8.0'
22
+ version: '8.2'
22
23
  type: :runtime
23
24
  prerelease: false
24
25
  version_requirements: !ruby/object:Gem::Requirement
25
26
  requirements:
26
27
  - - ">="
27
28
  - !ruby/object:Gem::Version
28
- version: '7.0'
29
+ version: '7.2'
29
30
  - - "<"
30
31
  - !ruby/object:Gem::Version
31
- version: '8.0'
32
+ version: '8.2'
32
33
  - !ruby/object:Gem::Dependency
33
34
  name: tailwindcss-rails
34
35
  requirement: !ruby/object:Gem::Requirement
@@ -187,6 +188,7 @@ files:
187
188
  - app/controllers/studio/email_images_controller.rb
188
189
  - app/controllers/studio/links_controller.rb
189
190
  - app/controllers/studio/local_emails_controller.rb
191
+ - app/controllers/studio/models_controller.rb
190
192
  - app/controllers/theme_settings_controller.rb
191
193
  - app/helpers/studio/admin_models_table_helper.rb
192
194
  - app/helpers/studio_email_delivery_helper.rb
@@ -204,6 +206,7 @@ files:
204
206
  - app/models/studio/email_delivery.rb
205
207
  - app/models/studio/enumeral.rb
206
208
  - app/models/studio/link.rb
209
+ - app/models/studio/model_page.rb
207
210
  - app/models/theme_setting.rb
208
211
  - app/services/google_oauth_validator.rb
209
212
  - app/services/magic_link.rb
@@ -258,6 +261,7 @@ files:
258
261
  - app/views/studio/modals/blocks/_processing_card.html.erb
259
262
  - app/views/studio/modals/blocks/_progress_countdown.html.erb
260
263
  - app/views/studio/modals/blocks/_success_card.html.erb
264
+ - app/views/studio/models/show.html.erb
261
265
  - app/views/theme_settings/edit.html.erb
262
266
  - app/views/user_mailer/magic_link.html.erb
263
267
  - app/views/user_mailer/magic_link.text.erb
@@ -293,6 +297,7 @@ metadata:
293
297
  source_code_uri: https://github.com/amcritchie/studio-engine/tree/main
294
298
  bug_tracker_uri: https://github.com/amcritchie/studio-engine/issues
295
299
  changelog_uri: https://github.com/amcritchie/studio-engine/blob/main/CHANGELOG.md
300
+ post_install_message:
296
301
  rdoc_options: []
297
302
  require_paths:
298
303
  - lib
@@ -307,7 +312,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
307
312
  - !ruby/object:Gem::Version
308
313
  version: '0'
309
314
  requirements: []
310
- rubygems_version: 4.0.9
315
+ rubygems_version: 3.5.22
316
+ signing_key:
311
317
  specification_version: 4
312
318
  summary: Shared Rails engine providing auth, SSO, error logging, theming, and S3-backed
313
319
  image caching