archipelago-rails 0.9.0 → 0.10.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.
Files changed (3) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +291 -48
  3. metadata +1 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3f916ab9e567301591e63ef33c6c2862e7fd545f27821d37d079c348fdf31489
4
- data.tar.gz: e1a648f35bdd36fee3c0c798989a613663a1bfd7c336757bd73005c60417f10a
3
+ metadata.gz: 7f06023adb9b3ece8f7310941796040dbf5047e9e7521ef0bff4884f5c1605ac
4
+ data.tar.gz: 7febff261479925f0afb35bb3f0f661da1b26c6ca0b74f1a774bd1f3ce4b0230
5
5
  SHA512:
6
- metadata.gz: ca210e1dd474885e9b322881e23ecc264ee038914de111080883e2198dd1a0dfe9f85eb6416ba72b7c6db9700fa61419003f94d6736043192c391ea2c1924317
7
- data.tar.gz: 846fdc9686466a6ad5ca9a98b74ae5ca3ba9286878f5cedbc9b0410a739988558be826d10b2d49282835e6a675e010757263d4051fb35970924937db47689e5a
6
+ metadata.gz: 0c80efe03b03d744c2e7f2463efd78d0b7cdf825e4e5ec888aef7ccf17ec48e3960ef550259aded82e639645f655719a7fdf76c6e63bed1ce083c519cdb66af5
7
+ data.tar.gz: ab7d10432d6e247b3bfce8d3ab76894467c978ecdbfd40af479f55b001ce1cbc802b438e1689f9f1bd0b1aac3d99b6785bd954e7f40a2a5abf8d67a94ff28fbb
data/README.md CHANGED
@@ -1,87 +1,330 @@
1
1
  # archipelago-rails
2
2
 
3
- Rails engine for Archipelago server-driven React islands.
3
+ Rails engine for Archipelago server-driven React islands with Inertia-style props, form handling, and real-time updates via ActionCable.
4
4
 
5
- ## Supported Rails versions
5
+ ## Install
6
+
7
+ Add to your Gemfile:
6
8
 
7
- - Rails `>= 7.1`
9
+ ```ruby
10
+ gem "archipelago-rails"
11
+ ```
8
12
 
9
- ## Development setup
13
+ Then run the install generator:
10
14
 
11
15
  ```bash
12
- bundle install
16
+ rails g archipelago:install
13
17
  ```
14
18
 
15
- ## Run tests
19
+ ### React setup (esbuild)
16
20
 
17
21
  ```bash
18
- # full suite (core + rails integration tests)
19
- bin/test
20
-
21
- # split tasks
22
- bundle exec rake test:core
23
- bundle exec rake test:rails
22
+ rails g archipelago:install:react
24
23
  ```
25
24
 
26
- ## Rails version matrix (Appraisal)
25
+ This scaffolds frontend bootstrap wiring. Options:
27
26
 
28
27
  ```bash
29
- bundle exec appraisal install
30
- bin/test-appraisal rails-7-1
31
- bin/test-appraisal rails-7-2
32
- bin/test-appraisal rails-8-1
28
+ rails g archipelago:install:react --interactive=false --bundler=esbuild --typescript=true
29
+ rails g archipelago:install:react --lazy_registry # dynamic imports instead of eager
30
+ rails g archipelago:install:react --install # install npm packages immediately
31
+ ```
32
+
33
+ ## Core Concepts
34
+
35
+ Archipelago lets you embed interactive React components ("islands") inside server-rendered Rails views. Each island receives **props from the server** and can call **server-side actions** that return updated props, errors, or redirects.
36
+
37
+ ```
38
+ ┌─────────────────────────────────────┐
39
+ │ Rails View │
40
+ │ ┌───────────────────┐ │
41
+ │ │ React Island │ ← props │
42
+ │ │ (TeamMembers) │ │
43
+ │ │ ┌─────────────┐ │ │
44
+ │ │ │ Add Member │──┼─→ Action │
45
+ │ │ │ Form │ │ (server) │
46
+ │ │ └─────────────┘ │ │
47
+ │ └───────────────────┘ │
48
+ └─────────────────────────────────────┘
33
49
  ```
34
50
 
35
- ## Notes
51
+ ## Building Actions
36
52
 
37
- - Controller/generator tests run against an in-test Rails application harness.
38
- - JS packages are tested from repository root via `yarn test`.
53
+ Actions live in `app/islands/<component>/` and handle requests from island components.
39
54
 
40
- ## Host app setup (React + esbuild)
55
+ ### Basic action
41
56
 
42
- After `rails g archipelago:install`, you can scaffold frontend bootstrap wiring:
57
+ ```ruby
58
+ # app/islands/team_members/add_member.rb
59
+ class TeamMembers::AddMember < Archipelago::Action
60
+ param :email, :string, required: true, strip: true, downcase: true
43
61
 
44
- ```bash
45
- rails g archipelago:install:react
62
+ authorize { current_user.admin? }
63
+
64
+ def perform
65
+ member = Team.find(raw_params[:team_id]).members.create!(email: email)
66
+
67
+ props(
68
+ members: Team.find(raw_params[:team_id]).members.map { |m| { id: m.id, email: m.email } }
69
+ )
70
+ end
71
+ end
46
72
  ```
47
73
 
48
- The generator writes `.npmrc` with:
74
+ ### Action lifecycle
75
+
76
+ 1. **Param coercion** — declared params are validated and coerced
77
+ 2. **Authorization** — the `authorize` block runs (raises `Forbidden` on failure)
78
+ 3. **`perform`** — your business logic executes
79
+ 4. **Response** — returns `ok` (with props), `error` (with field errors), `redirect`, or `forbidden`
80
+
81
+ ### Response helpers
82
+
83
+ ```ruby
84
+ def perform
85
+ # Return updated props
86
+ props(members: [...])
87
+
88
+ # Or redirect
89
+ redirect_to "/teams/#{team.id}"
49
90
 
50
- ```text
51
- @archipelago-js:registry=https://registry.npmjs.org
91
+ # Or add field errors
92
+ add_error(:email, "is already taken")
93
+ end
52
94
  ```
53
95
 
54
- This keeps installs reliable across npm, Yarn classic, pnpm, and bun.
96
+ ### `current_user`
55
97
 
56
- By default this runs an interactive wizard with auto-detected defaults
57
- (bundler, TypeScript, package manager, and local monorepo path).
98
+ Available in all actions, delegating to the configured user method:
58
99
 
59
- For esbuild apps, the wizard also enables auto-registry by default:
60
- - scans `app/javascript/islands/**/*.{js,jsx,ts,tsx}`
61
- - writes `app/javascript/archipelago/registry.generated.(js|ts)`
62
- - wires `package.json` esbuild scripts to run generator first
100
+ ```ruby
101
+ def perform
102
+ team = current_user.teams.find(raw_params[:team_id])
103
+ # ...
104
+ end
105
+ ```
63
106
 
64
- Useful options:
107
+ ### ActiveRecord::RecordInvalid
65
108
 
66
- ```bash
67
- # disable prompts and use flags only
68
- rails g archipelago:install:react --interactive=false --bundler=esbuild --typescript=true
109
+ Archipelago automatically catches `ActiveRecord::RecordInvalid` exceptions and maps them to field-level error responses.
69
110
 
70
- # force TSX output
71
- rails g archipelago:install:react --typescript=true
111
+ ## Params DSL
72
112
 
73
- # disable auto-registry and keep manual registry map in entry file
74
- rails g archipelago:install:react --auto_registry=false
113
+ Declare expected parameters with type coercion, validation, and transformation:
75
114
 
76
- # install npm packages immediately
77
- rails g archipelago:install:react --install
115
+ ```ruby
116
+ class TeamMembers::UpdateSettings < Archipelago::Action
117
+ param :name, :string, required: true, strip: true, min: 2, max: 100
118
+ param :email, :string, required: true, format: /\A[^@\s]+@[^@\s]+\z/
119
+ param :role, :string, required: true, in: %w[admin member viewer]
120
+ param :bio, :string, empty_as_nil: true
121
+ param :age, :integer, min: 13, max: 150
122
+ param :score, :float
123
+ param :active, :boolean, default: true
124
+ param :tags, :array, of: :string
125
+ param :metadata, :json
126
+ param :starts_on, :date
127
+ param :due_at, :datetime
128
+ param :nickname, :string, validate: ->(v) { "is offensive" if offensive?(v) }
78
129
 
79
- # install Archipelago packages from a local monorepo path
80
- rails g archipelago:install:react --install --local-monorepo-path=/absolute/path/to/cdx
130
+ # Params become methods: name, email, role, bio, etc.
131
+ def perform
132
+ user = current_user
133
+ user.update!(name: name, email: email, role: role, bio: bio)
134
+ props(user: serialize(user))
135
+ end
136
+ end
81
137
  ```
82
138
 
83
- Manual refresh command (if needed while a long-running watch is already running):
139
+ ### Supported types
140
+
141
+ | Type | Coercion |
142
+ |------|----------|
143
+ | `:string` | `String(value)` |
144
+ | `:integer` | `Integer(value)` |
145
+ | `:float` | `Float(value)` |
146
+ | `:boolean` | `true/1/"1"/"true"/"on"/"yes"` → `true`, etc. |
147
+ | `:date` | `Date.parse(value)` |
148
+ | `:datetime` | `Time.parse(value)` |
149
+ | `:array` | Pass-through or `JSON.parse`, with optional `of:` typed elements |
150
+ | `:json` | Pass-through or `JSON.parse` |
151
+
152
+ ### Validation options
153
+
154
+ | Option | Description |
155
+ |--------|-------------|
156
+ | `required: true` | Rejects blank/nil values |
157
+ | `default: value` | Fallback when missing (supports lambdas) |
158
+ | `strip: true` | Strip whitespace (strings only) |
159
+ | `downcase: true` | Downcase (strings only) |
160
+ | `upcase: true` | Upcase (strings only) |
161
+ | `in: [...]` | Value must be in the list |
162
+ | `format: /regex/` | String must match pattern |
163
+ | `min: n` | Minimum value or length |
164
+ | `max: n` | Maximum value or length |
165
+ | `empty_as_nil: true` | Treat `""` / whitespace-only as `nil` |
166
+ | `of: :type` | Element type for arrays |
167
+ | `validate: ->(v) { ... }` | Custom validator; return error string or nil |
168
+
169
+ ## Authorization
170
+
171
+ ### Per-action authorization
172
+
173
+ Every action should define an `authorize` block:
174
+
175
+ ```ruby
176
+ class TeamMembers::AddMember < Archipelago::Action
177
+ authorize { current_user.admin? }
178
+
179
+ def perform
180
+ # ...
181
+ end
182
+ end
183
+ ```
184
+
185
+ When `authorize_by_default` is `true` (the default), actions without an `authorize` block raise `MissingAuthorization`.
186
+
187
+ ### Pundit adapter
188
+
189
+ Include `Archipelago::PunditAdapter` for Pundit-style authorization:
190
+
191
+ ```ruby
192
+ class TeamMembers::AddMember < Archipelago::Action
193
+ include Archipelago::PunditAdapter
194
+
195
+ authorize { authorize(@team, :add_member?) }
196
+
197
+ def perform
198
+ @team = Team.find(raw_params[:team_id])
199
+ authorize(@team) # infers query from action name
200
+ # ...
201
+ end
202
+ end
203
+ ```
204
+
205
+ The adapter provides:
206
+ - `authorize(record, query = nil)` — raises `Forbidden` if policy denies
207
+ - `policy(record)` — returns the policy instance
208
+
209
+ ### CanCan adapter
210
+
211
+ Include `Archipelago::CanCanAdapter` for CanCan-style authorization:
212
+
213
+ ```ruby
214
+ class TeamMembers::AddMember < Archipelago::Action
215
+ include Archipelago::CanCanAdapter
216
+
217
+ def perform
218
+ team = Team.find(raw_params[:team_id])
219
+ authorize!(:manage, team)
220
+ # ...
221
+ end
222
+ end
223
+ ```
224
+
225
+ The adapter provides:
226
+ - `authorize!(action, record)` — raises `Forbidden` if ability denies
227
+ - `current_ability` — returns the ability instance
228
+
229
+ Configure the ability builder if you don't use a top-level `Ability` class:
230
+
231
+ ```ruby
232
+ Archipelago.configure do |config|
233
+ config.current_ability = ->(user) { CustomAbility.new(user) }
234
+ end
235
+ ```
236
+
237
+ ## Stream Authorization
238
+
239
+ ActionCable streams can be authorized before subscription:
240
+
241
+ ```ruby
242
+ Archipelago.configure do |config|
243
+ config.stream_authorizer = ->(connection:, stream_name:, params:) {
244
+ user = connection.current_user
245
+ # stream_name is e.g. "TeamMembers:42"
246
+ team_id = stream_name.split(":").last.to_i
247
+ user.teams.exists?(id: team_id)
248
+ }
249
+
250
+ # Reject all streams that don't pass through the authorizer
251
+ config.require_stream_authorization = true
252
+ end
253
+ ```
254
+
255
+ When `require_stream_authorization` is `true`, any stream without a configured authorizer is rejected. When `false` (default), streams are allowed unless an authorizer explicitly denies them.
256
+
257
+ **Important:** If your streams carry tenant-specific or user-specific data, always configure a `stream_authorizer` or enable `require_stream_authorization`.
258
+
259
+ ## Configuration
260
+
261
+ ```ruby
262
+ # config/initializers/archipelago.rb
263
+ Archipelago.configure do |config|
264
+ config.root_namespace = "Islands" # where actions live under app/islands/
265
+ config.current_user_method = :current_user # controller method for current user
266
+ config.authorize_by_default = true # require authorize blocks
267
+ config.strict_origin_check = false # validate redirect origins
268
+ config.allowed_redirect_hosts = [] # allowed redirect hosts
269
+ config.stream_authorizer = nil # ActionCable stream auth lambda
270
+ config.require_stream_authorization = false # reject unauthed streams
271
+ config.current_ability = nil # CanCan ability builder
272
+ end
273
+ ```
274
+
275
+ ## Response Contract
276
+
277
+ All action responses follow a standard JSON shape:
278
+
279
+ ```json
280
+ // ok — updated props
281
+ { "status": "ok", "props": { ... }, "version": 1716000000000 }
282
+
283
+ // error — field-level validation errors
284
+ { "status": "error", "errors": { "email": ["can't be blank"] } }
285
+
286
+ // redirect
287
+ { "status": "redirect", "location": "/teams/1" }
288
+
289
+ // forbidden
290
+ { "status": "forbidden" }
291
+ ```
292
+
293
+ The `version` field is a monotonic timestamp used by the client to prevent stale broadcasts from overwriting newer data.
294
+
295
+ ## Streams & Broadcasting
296
+
297
+ When a client sends the `X-Archipelago-Stream` header (or the legacy `__stream` param), successful action responses are automatically broadcast to all subscribers of that stream.
298
+
299
+ On the client side, `useIslandProps({ stream: "TeamMembers:42" })` auto-subscribes to the stream and merges broadcast props into the component.
300
+
301
+ ## Supported Rails versions
302
+
303
+ - Rails `>= 7.1`, `< 9.0`
304
+
305
+ ## Development
84
306
 
85
307
  ```bash
86
- yarn archipelago:registry
308
+ bundle install
87
309
  ```
310
+
311
+ ### Run tests
312
+
313
+ ```bash
314
+ bin/test # full suite
315
+ bundle exec rake test:core # core unit tests
316
+ bundle exec rake test:rails # rails integration tests
317
+ ```
318
+
319
+ ### Rails version matrix (Appraisal)
320
+
321
+ ```bash
322
+ bundle exec appraisal install
323
+ bin/test-appraisal rails-7-1
324
+ bin/test-appraisal rails-7-2
325
+ bin/test-appraisal rails-8-1
326
+ ```
327
+
328
+ ## License
329
+
330
+ MIT
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: archipelago-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.0
4
+ version: 0.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Archipelago