studio-engine 0.11.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: 4d4a633071c04010dadfb92d3beef1a7e075a90c94e55ab5761a03a114f48f40
4
- data.tar.gz: fc17d48b30cfcbde79e4c3bdc5bf618789a7765dbc359a134b28800cc772429e
3
+ metadata.gz: 61bf440c6e7573b35e8d7a680851d2535b37b08690f36c0f8c42402a8a1fcc64
4
+ data.tar.gz: e79e46ab92bcd13bf5655526c199c1d7a1b17610d815dc240a5af7250455b9e1
5
5
  SHA512:
6
- metadata.gz: e32e14a996202c61dd230a4084927539618341c0343bbafad1066280394da6d84976222a5621000157d6e84df83452c6a5f234f36ca11e18c408e340757a0b49
7
- data.tar.gz: 8592453e2efdbad7c3df75bfb13274a6abfa080f9de9e6188a70d90e935d2dfa1446c788f15d55eab467207bcfa9cef129bf9b0abf3fbb001e846a11a6c31f3f
6
+ metadata.gz: 64d04f5527318728c090ad318d9d5f652a12b1e8eadc6f665eccd4a0c4a0aadff6d1c7dfaecdc5c2a438c08b648a34ba78f1869dc8df1b80c6d1ccc9728f457d
7
+ data.tar.gz: bba2fac4175596d6448dcc9611c9c4740f021fcfb467c09986a20d0d506e4c37cc933257ac6e0d23deb262a0fcf98baa3fe3bce104a8985e85562451ddab406f
data/CHANGELOG.md CHANGED
@@ -4,6 +4,19 @@ 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
+
7
20
  ## 0.11.0 — 2026-06-24
8
21
 
9
22
  ### Changed
@@ -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.11.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
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.11.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
@@ -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