archipelago-rails 0.1.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 +7 -0
- data/Appraisals +16 -0
- data/LICENSE.txt +21 -0
- data/README.md +79 -0
- data/Rakefile +17 -0
- data/app/controllers/archipelago/application_controller.rb +10 -0
- data/app/controllers/archipelago/islands_controller.rb +158 -0
- data/config/database.yml +5 -0
- data/config/routes.rb +6 -0
- data/lib/archipelago/action.rb +121 -0
- data/lib/archipelago/broadcasts.rb +17 -0
- data/lib/archipelago/channel.rb +36 -0
- data/lib/archipelago/configuration.rb +23 -0
- data/lib/archipelago/context.rb +14 -0
- data/lib/archipelago/engine.rb +21 -0
- data/lib/archipelago/params_dsl.rb +143 -0
- data/lib/archipelago/registry.rb +25 -0
- data/lib/archipelago/resolver.rb +54 -0
- data/lib/archipelago/response.rb +35 -0
- data/lib/archipelago/security/origin_validator.rb +32 -0
- data/lib/archipelago/security/redirect_validator.rb +30 -0
- data/lib/archipelago/test_helpers.rb +31 -0
- data/lib/archipelago/view_helper.rb +31 -0
- data/lib/archipelago-rails.rb +3 -0
- data/lib/archipelago.rb +71 -0
- data/lib/generators/archipelago/install/install_generator.rb +43 -0
- data/lib/generators/archipelago/install/react/react_generator.rb +380 -0
- data/lib/generators/archipelago/install/react/templates/entry.js.tt +13 -0
- data/lib/generators/archipelago/install/react/templates/generate_registry.mjs.tt +96 -0
- data/lib/generators/archipelago/install/react_generator.rb +3 -0
- data/lib/generators/archipelago/install_generator.rb +3 -0
- data/lib/generators/archipelago/island/island_generator.rb +44 -0
- data/lib/generators/archipelago/island/templates/action.rb.tt +11 -0
- data/lib/generators/archipelago/island/templates/component.tsx.tt +14 -0
- data/lib/generators/archipelago/island_generator.rb +3 -0
- data/test/archipelago/action_test.rb +136 -0
- data/test/archipelago/broadcasts_test.rb +29 -0
- data/test/archipelago/channel_test.rb +15 -0
- data/test/archipelago/notifications_test.rb +60 -0
- data/test/archipelago/origin_validator_test.rb +36 -0
- data/test/archipelago/params_dsl_test.rb +51 -0
- data/test/archipelago/redirect_validator_test.rb +28 -0
- data/test/archipelago/resolver_test.rb +80 -0
- data/test/archipelago/response_test.rb +30 -0
- data/test/archipelago/view_helper_test.rb +32 -0
- data/test/controllers/islands_controller_test.rb +115 -0
- data/test/generators/install_generator_test.rb +26 -0
- data/test/generators/island_generator_test.rb +20 -0
- data/test/generators/react_install_generator_test.rb +67 -0
- data/test/test_helper.rb +35 -0
- metadata +180 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: a271d60fbd7122dfe75a111e3182fc34662dd3afe9b6d4183e834d08be828e2b
|
|
4
|
+
data.tar.gz: '0357780015635cb62d1255e257064d6b5a3674a41f18465f5ce6f0e59ec54759'
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: a40da1f08277ed421fa36b9d29e03223036259625689eb17489455158d0f056cb4600e633f19994bcb2ba6bb7e06773a1db40b5033740f99027baaecd5aa0e66
|
|
7
|
+
data.tar.gz: b0827ebf94625a2a5c7f21cdf1c870794609a01d20e8a8dc028ef108cf1f1decde1f5d6284bba7930a267b85e9770430e19e63e45059312c6fd8b911e305add1
|
data/Appraisals
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
appraise "rails-7-1" do
|
|
4
|
+
gem "rails", "~> 7.1.0"
|
|
5
|
+
gem "sqlite3", ">= 1.6"
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
appraise "rails-7-2" do
|
|
9
|
+
gem "rails", "~> 7.2.0"
|
|
10
|
+
gem "sqlite3", ">= 1.6"
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
appraise "rails-8-1" do
|
|
14
|
+
gem "rails", "~> 8.1.0"
|
|
15
|
+
gem "sqlite3", ">= 1.6"
|
|
16
|
+
end
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Archipelago
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# archipelago-rails
|
|
2
|
+
|
|
3
|
+
Rails engine for Archipelago server-driven React islands.
|
|
4
|
+
|
|
5
|
+
## Supported Rails versions
|
|
6
|
+
|
|
7
|
+
- Rails `>= 7.1`
|
|
8
|
+
|
|
9
|
+
## Development setup
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
bundle install
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Run tests
|
|
16
|
+
|
|
17
|
+
```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
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Rails version matrix (Appraisal)
|
|
27
|
+
|
|
28
|
+
```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
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Notes
|
|
36
|
+
|
|
37
|
+
- Controller/generator tests run against an in-test Rails application harness.
|
|
38
|
+
- JS packages are tested from repository root via `yarn test`.
|
|
39
|
+
|
|
40
|
+
## Host app setup (React + esbuild)
|
|
41
|
+
|
|
42
|
+
After `rails g archipelago:install`, you can scaffold frontend bootstrap wiring:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
rails g archipelago:install:react
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
By default this runs an interactive wizard with auto-detected defaults
|
|
49
|
+
(bundler, TypeScript, package manager, and local monorepo path).
|
|
50
|
+
|
|
51
|
+
For esbuild apps, the wizard also enables auto-registry by default:
|
|
52
|
+
- scans `app/javascript/islands/**/*.{js,jsx,ts,tsx}`
|
|
53
|
+
- writes `app/javascript/archipelago/registry.generated.(js|ts)`
|
|
54
|
+
- wires `package.json` esbuild scripts to run generator first
|
|
55
|
+
|
|
56
|
+
Useful options:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
# disable prompts and use flags only
|
|
60
|
+
rails g archipelago:install:react --interactive=false --bundler=esbuild --typescript=true
|
|
61
|
+
|
|
62
|
+
# force TSX output
|
|
63
|
+
rails g archipelago:install:react --typescript=true
|
|
64
|
+
|
|
65
|
+
# disable auto-registry and keep manual registry map in entry file
|
|
66
|
+
rails g archipelago:install:react --auto_registry=false
|
|
67
|
+
|
|
68
|
+
# install npm packages immediately
|
|
69
|
+
rails g archipelago:install:react --install
|
|
70
|
+
|
|
71
|
+
# install Archipelago packages from a local monorepo path
|
|
72
|
+
rails g archipelago:install:react --install --local-monorepo-path=/absolute/path/to/cdx
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Manual refresh command (if needed while a long-running watch is already running):
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
yarn archipelago:registry
|
|
79
|
+
```
|
data/Rakefile
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rake/testtask"
|
|
4
|
+
|
|
5
|
+
Rake::TestTask.new("test:core") do |t|
|
|
6
|
+
t.libs << "test"
|
|
7
|
+
t.pattern = "test/archipelago/**/*_test.rb"
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
Rake::TestTask.new("test:rails") do |t|
|
|
11
|
+
t.libs << "test"
|
|
12
|
+
t.pattern = ["test/controllers/**/*_test.rb", "test/generators/**/*_test.rb"]
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
task test: ["test:core", "test:rails"]
|
|
16
|
+
|
|
17
|
+
task default: :test
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "action_controller/base"
|
|
4
|
+
|
|
5
|
+
module Archipelago
|
|
6
|
+
class ApplicationController < ActionController::Base
|
|
7
|
+
protect_from_forgery with: :exception
|
|
8
|
+
skip_forgery_protection if defined?(Rails) && Rails.env.test?
|
|
9
|
+
end
|
|
10
|
+
end
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Archipelago
|
|
4
|
+
class IslandsController < ApplicationController
|
|
5
|
+
rescue_from Archipelago::ResolutionError, with: :render_not_found
|
|
6
|
+
rescue_from Archipelago::MissingAuthorization, with: :render_forbidden
|
|
7
|
+
rescue_from Archipelago::Forbidden, with: :render_forbidden
|
|
8
|
+
rescue_from Archipelago::InvalidOrigin, with: :render_forbidden
|
|
9
|
+
rescue_from Archipelago::InvalidRedirect, with: :render_invalid_redirect
|
|
10
|
+
|
|
11
|
+
def create
|
|
12
|
+
validate_origin!
|
|
13
|
+
|
|
14
|
+
action_class = resolver.resolve(component: params[:component], operation: params[:operation])
|
|
15
|
+
action = action_class.new(ctx: request_context, raw_params: island_params)
|
|
16
|
+
payload = action.call
|
|
17
|
+
|
|
18
|
+
render json: payload, status: rack_status_for(payload)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def debug
|
|
22
|
+
return head :not_found unless Rails.env.development? || Rails.env.test?
|
|
23
|
+
|
|
24
|
+
render json: debug_payload
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def resolver
|
|
30
|
+
@resolver ||= Archipelago::Resolver.new
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def request_context
|
|
34
|
+
Archipelago::Context.new(
|
|
35
|
+
request: request,
|
|
36
|
+
params: params,
|
|
37
|
+
session: session,
|
|
38
|
+
user: current_archipelago_user
|
|
39
|
+
)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def current_archipelago_user
|
|
43
|
+
resolver_proc = Archipelago.configuration.current_user_resolver
|
|
44
|
+
return instance_exec(&resolver_proc) if resolver_proc
|
|
45
|
+
|
|
46
|
+
method_name = Archipelago.configuration.current_user_method
|
|
47
|
+
return send(method_name) if respond_to?(method_name, true)
|
|
48
|
+
|
|
49
|
+
nil
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def validate_origin!
|
|
53
|
+
Archipelago::Security::OriginValidator.new(request).validate!
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def island_params
|
|
57
|
+
params.to_unsafe_h.except(:component, :operation, :controller, :action)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def rack_status_for(payload)
|
|
61
|
+
case payload[:status]
|
|
62
|
+
when "ok", "redirect", "error"
|
|
63
|
+
:ok
|
|
64
|
+
when "forbidden"
|
|
65
|
+
:forbidden
|
|
66
|
+
else
|
|
67
|
+
:ok
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def render_not_found
|
|
72
|
+
head :not_found
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def render_forbidden
|
|
76
|
+
render json: Archipelago::Response.forbidden, status: :forbidden
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def render_invalid_redirect(exception)
|
|
80
|
+
render json: Archipelago::Response.error(errors: { base: [exception.message] }), status: :unprocessable_entity
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def debug_payload
|
|
84
|
+
{
|
|
85
|
+
root_namespace: Archipelago.configuration.root_namespace,
|
|
86
|
+
registry: Archipelago.registry.to_h.transform_values(&:name),
|
|
87
|
+
actions: debug_actions_from_files,
|
|
88
|
+
registry_actions: debug_actions_from_registry
|
|
89
|
+
}
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def debug_actions_from_files
|
|
93
|
+
files = Dir.glob(Rails.root.join("app/islands/**/*.rb")).sort
|
|
94
|
+
|
|
95
|
+
files.map do |file_path|
|
|
96
|
+
relative = file_path.delete_prefix("#{Rails.root}/app/islands/").delete_suffix(".rb")
|
|
97
|
+
*component_parts, operation = relative.split("/")
|
|
98
|
+
component = component_parts.map(&:camelize).join("__")
|
|
99
|
+
handler_name = [
|
|
100
|
+
Archipelago.configuration.root_namespace,
|
|
101
|
+
*component_parts.map(&:camelize),
|
|
102
|
+
operation.camelize
|
|
103
|
+
].join("::")
|
|
104
|
+
handler_class = handler_name.safe_constantize
|
|
105
|
+
|
|
106
|
+
{
|
|
107
|
+
source: "filesystem",
|
|
108
|
+
component: component,
|
|
109
|
+
operation: operation,
|
|
110
|
+
file: file_path.delete_prefix("#{Rails.root}/"),
|
|
111
|
+
handler: handler_name,
|
|
112
|
+
params: debug_param_schema_for(handler_class)
|
|
113
|
+
}
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def debug_actions_from_registry
|
|
118
|
+
Archipelago.registry.to_h.map do |key, handler|
|
|
119
|
+
component, operation = key.split("#", 2)
|
|
120
|
+
{
|
|
121
|
+
source: "registry",
|
|
122
|
+
component: component,
|
|
123
|
+
operation: operation,
|
|
124
|
+
handler: handler.name,
|
|
125
|
+
params: debug_param_schema_for(handler)
|
|
126
|
+
}
|
|
127
|
+
end.sort_by { |entry| [entry[:component].to_s, entry[:operation].to_s] }
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def debug_param_schema_for(handler_class)
|
|
131
|
+
return [] unless handler_class.respond_to?(:param_definitions)
|
|
132
|
+
|
|
133
|
+
handler_class.param_definitions.values.map do |definition|
|
|
134
|
+
{
|
|
135
|
+
name: definition.name.to_s,
|
|
136
|
+
type: definition.type.to_s,
|
|
137
|
+
required: definition.required,
|
|
138
|
+
default: debug_param_default(definition.default),
|
|
139
|
+
transforms: {
|
|
140
|
+
strip: definition.strip,
|
|
141
|
+
downcase: definition.downcase,
|
|
142
|
+
upcase: definition.upcase
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def debug_param_default(value)
|
|
149
|
+
if value == Archipelago::ParamsDSL::MISSING
|
|
150
|
+
{ provided: false }
|
|
151
|
+
elsif value.respond_to?(:call)
|
|
152
|
+
{ provided: true, kind: "callable" }
|
|
153
|
+
else
|
|
154
|
+
{ provided: true, value: value }
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
data/config/database.yml
ADDED
data/config/routes.rb
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Archipelago
|
|
4
|
+
class Action
|
|
5
|
+
include Archipelago::ParamsDSL
|
|
6
|
+
|
|
7
|
+
class << self
|
|
8
|
+
attr_reader :authorization_block
|
|
9
|
+
|
|
10
|
+
def authorize(&block)
|
|
11
|
+
@authorization_block = block
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
attr_reader :ctx, :raw_params, :errors
|
|
16
|
+
|
|
17
|
+
def initialize(ctx:, raw_params:)
|
|
18
|
+
@ctx = ctx
|
|
19
|
+
@raw_params = raw_params.with_indifferent_access
|
|
20
|
+
@errors = Hash.new { |hash, key| hash[key] = [] }
|
|
21
|
+
@coerced_params = {}
|
|
22
|
+
@response_props = {}
|
|
23
|
+
@redirect_location = nil
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def call
|
|
27
|
+
Archipelago.instrument("archipelago.action.perform", action: self.class.name) do
|
|
28
|
+
@coerced_params, coercion_errors = coerce_declared_params(raw_params)
|
|
29
|
+
merge_errors!(coercion_errors)
|
|
30
|
+
if errors.any?
|
|
31
|
+
Archipelago.instrument("archipelago.action.error", action: self.class.name, reason: "validation")
|
|
32
|
+
return Archipelago::Response.error(errors: errors)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
run_authorization!
|
|
36
|
+
perform
|
|
37
|
+
|
|
38
|
+
if errors.any?
|
|
39
|
+
Archipelago.instrument("archipelago.action.error", action: self.class.name, reason: "validation")
|
|
40
|
+
return Archipelago::Response.error(errors: errors)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
if @redirect_location
|
|
44
|
+
validator = Archipelago::Security::RedirectValidator.new
|
|
45
|
+
location = validator.validate!(@redirect_location)
|
|
46
|
+
return Archipelago::Response.redirect(location: location)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
payload = Archipelago::Response.ok(props: @response_props, version: Archipelago.next_version)
|
|
50
|
+
maybe_broadcast(payload)
|
|
51
|
+
payload
|
|
52
|
+
end
|
|
53
|
+
rescue Archipelago::Forbidden
|
|
54
|
+
Archipelago.instrument("archipelago.action.error", action: self.class.name, reason: "forbidden")
|
|
55
|
+
Archipelago::Response.forbidden
|
|
56
|
+
rescue StandardError => e
|
|
57
|
+
if record_invalid_error?(e)
|
|
58
|
+
map_record_invalid!(e)
|
|
59
|
+
Archipelago.instrument("archipelago.action.error", action: self.class.name, reason: "record_invalid")
|
|
60
|
+
Archipelago::Response.error(errors: errors)
|
|
61
|
+
else
|
|
62
|
+
raise
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def add_error(field, message)
|
|
67
|
+
errors[field.to_s] << message
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def props(payload)
|
|
71
|
+
@response_props = payload
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def redirect_to(location)
|
|
75
|
+
@redirect_location = location
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
def run_authorization!
|
|
81
|
+
block = self.class.authorization_block
|
|
82
|
+
|
|
83
|
+
if block.nil?
|
|
84
|
+
raise Archipelago::MissingAuthorization if Archipelago.configuration.authorize_by_default
|
|
85
|
+
|
|
86
|
+
return true
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
authorized = instance_exec(&block)
|
|
90
|
+
raise Archipelago::Forbidden unless authorized
|
|
91
|
+
|
|
92
|
+
true
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def merge_errors!(incoming)
|
|
96
|
+
incoming.each do |field, messages|
|
|
97
|
+
messages.each { |message| add_error(field, message) }
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def map_record_invalid!(error)
|
|
102
|
+
return unless error.record.respond_to?(:errors)
|
|
103
|
+
|
|
104
|
+
error.record.errors.to_hash(true).each do |field, messages|
|
|
105
|
+
Array(messages).each { |message| add_error(field, message) }
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def record_invalid_error?(error)
|
|
110
|
+
defined?(ActiveRecord::RecordInvalid) && error.is_a?(ActiveRecord::RecordInvalid)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def maybe_broadcast(payload)
|
|
114
|
+
stream_name = raw_params[:__stream]
|
|
115
|
+
return if stream_name.blank?
|
|
116
|
+
return unless payload[:status] == "ok"
|
|
117
|
+
|
|
118
|
+
Archipelago.broadcast(stream_name, props: payload[:props], version: payload[:version])
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Archipelago
|
|
4
|
+
module Broadcasts
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
def broadcast(stream_name, props:, version: Archipelago.next_version)
|
|
8
|
+
payload = Archipelago::Response.ok(props: props, version: version)
|
|
9
|
+
unless defined?(ActionCable) && ActionCable.respond_to?(:server)
|
|
10
|
+
raise LoadError, "ActionCable is required for streaming broadcasts"
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
ActionCable.server.broadcast(stream_name, payload)
|
|
14
|
+
payload
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
base_channel = if defined?(::ApplicationCable::Channel)
|
|
4
|
+
::ApplicationCable::Channel
|
|
5
|
+
elsif defined?(ActionCable::Channel::Base)
|
|
6
|
+
ActionCable::Channel::Base
|
|
7
|
+
else
|
|
8
|
+
Class.new do
|
|
9
|
+
def stream_from(*); end
|
|
10
|
+
def reject; end
|
|
11
|
+
def params = {}
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
channel_class = Class.new(base_channel) do
|
|
16
|
+
def subscribed
|
|
17
|
+
stream_name = verified_stream_name
|
|
18
|
+
reject unless stream_name
|
|
19
|
+
|
|
20
|
+
stream_from(stream_name)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def verified_stream_name
|
|
26
|
+
stream_name = params[:stream_name].to_s
|
|
27
|
+
return nil unless stream_name.match?(self.class::STREAM_PATTERN)
|
|
28
|
+
|
|
29
|
+
stream_name
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
channel_class.const_set(:STREAM_PATTERN, /\A[A-Za-z0-9:_-]+\z/)
|
|
34
|
+
|
|
35
|
+
Archipelago.send(:remove_const, :IslandChannel) if Archipelago.const_defined?(:IslandChannel, false)
|
|
36
|
+
Archipelago.const_set(:IslandChannel, channel_class)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Archipelago
|
|
4
|
+
class Configuration
|
|
5
|
+
attr_accessor :root_namespace,
|
|
6
|
+
:current_user_method,
|
|
7
|
+
:current_user_resolver,
|
|
8
|
+
:authorize_by_default,
|
|
9
|
+
:strict_origin_check,
|
|
10
|
+
:allowed_redirect_hosts,
|
|
11
|
+
:version_source
|
|
12
|
+
|
|
13
|
+
def initialize
|
|
14
|
+
@root_namespace = "Islands"
|
|
15
|
+
@current_user_method = :current_user
|
|
16
|
+
@current_user_resolver = nil
|
|
17
|
+
@authorize_by_default = true
|
|
18
|
+
@strict_origin_check = false
|
|
19
|
+
@allowed_redirect_hosts = []
|
|
20
|
+
@version_source = -> { (Process.clock_gettime(Process::CLOCK_REALTIME, :millisecond)).to_i }
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Archipelago
|
|
4
|
+
class Context
|
|
5
|
+
attr_reader :request, :params, :session, :user
|
|
6
|
+
|
|
7
|
+
def initialize(request:, params:, session:, user: nil)
|
|
8
|
+
@request = request
|
|
9
|
+
@params = params
|
|
10
|
+
@session = session
|
|
11
|
+
@user = user
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/engine"
|
|
4
|
+
|
|
5
|
+
module Archipelago
|
|
6
|
+
class Engine < ::Rails::Engine
|
|
7
|
+
isolate_namespace Archipelago
|
|
8
|
+
|
|
9
|
+
initializer "archipelago.view_helper" do
|
|
10
|
+
ActiveSupport.on_load(:action_view) do
|
|
11
|
+
include Archipelago::ViewHelper
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
initializer "archipelago.test_helpers" do
|
|
16
|
+
ActiveSupport.on_load(:action_dispatch_integration_test) do
|
|
17
|
+
include Archipelago::TestHelpers
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|