seams 0.1.0 → 0.2.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 +4 -4
- data/CHANGELOG.md +212 -12
- data/README.md +235 -82
- data/lib/generators/seams/accounts/accounts_generator.rb +31 -3
- data/lib/generators/seams/accounts/templates/app/controllers/memberships_controller.rb.tt +100 -0
- data/lib/generators/seams/accounts/templates/app/views/memberships/index.html.erb.tt +52 -0
- data/lib/generators/seams/accounts/templates/config/routes.rb.tt +10 -4
- data/lib/generators/seams/accounts/templates/lib/concerns/authorization.rb.tt +35 -2
- data/lib/generators/seams/accounts/templates/lib/engine.rb.tt +13 -0
- data/lib/generators/seams/accounts/templates/spec/runtime/authorization_spec.rb.tt +102 -0
- data/lib/generators/seams/accounts/templates/spec/runtime/memberships_flow_spec.rb.tt +145 -0
- data/lib/generators/seams/admin/admin_generator.rb +51 -4
- data/lib/generators/seams/admin/templates/README.md.tt +33 -3
- data/lib/generators/seams/admin/templates/app/controllers/admin/application_controller.rb.tt +156 -8
- data/lib/generators/seams/admin/templates/app/dashboards/admin/account_dashboard.rb.tt +9 -1
- data/lib/generators/seams/admin/templates/app/dashboards/admin/accounts_membership_dashboard.rb.tt +9 -1
- data/lib/generators/seams/admin/templates/app/dashboards/admin/identity_dashboard.rb.tt +6 -0
- data/lib/generators/seams/admin/templates/app/dashboards/admin/invitation_dashboard.rb.tt +7 -1
- data/lib/generators/seams/admin/templates/app/dashboards/admin/invoice_dashboard.rb.tt +6 -0
- data/lib/generators/seams/admin/templates/app/dashboards/admin/lifetime_pass_dashboard.rb.tt +6 -0
- data/lib/generators/seams/admin/templates/app/dashboards/admin/notification_dashboard.rb.tt +6 -0
- data/lib/generators/seams/admin/templates/app/dashboards/admin/notification_preference_dashboard.rb.tt +6 -0
- data/lib/generators/seams/admin/templates/app/dashboards/admin/plan_dashboard.rb.tt +6 -0
- data/lib/generators/seams/admin/templates/app/dashboards/admin/subscription_dashboard.rb.tt +6 -0
- data/lib/generators/seams/admin/templates/app/dashboards/admin/team_dashboard.rb.tt +12 -2
- data/lib/generators/seams/admin/templates/app/dashboards/admin/teams_membership_dashboard.rb.tt +7 -1
- data/lib/generators/seams/admin/templates/app/fields/admin/fields/belongs_to.rb.tt +12 -0
- data/lib/generators/seams/admin/templates/app/fields/admin/fields/dashboard_option.rb.tt +22 -0
- data/lib/generators/seams/admin/templates/app/fields/admin/fields/has_many.rb.tt +12 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/tenant/account_policy.rb.tt +4 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/tenant/accounts_membership_policy.rb.tt +6 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/tenant/application_policy.rb.tt +51 -10
- data/lib/generators/seams/admin/templates/app/policies/admin/tenant/identity_policy.rb.tt +4 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/tenant/invitation_policy.rb.tt +6 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/tenant/invoice_policy.rb.tt +6 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/tenant/lifetime_pass_policy.rb.tt +6 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/tenant/notification_policy.rb.tt +6 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/tenant/notification_preference_policy.rb.tt +6 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/tenant/plan_policy.rb.tt +7 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/tenant/subscription_policy.rb.tt +6 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/tenant/team_policy.rb.tt +6 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/tenant/teams_membership_policy.rb.tt +6 -0
- data/lib/generators/seams/admin/templates/app/views/seams/admin/application/_index_header.html.erb.tt +57 -0
- data/lib/generators/seams/admin/templates/app/views/seams/admin/application/_navigation.html.erb.tt +14 -0
- data/lib/generators/seams/admin/templates/config/routes.rb.tt +24 -14
- data/lib/generators/seams/admin/templates/lib/concerns/authenticator.rb.tt +3 -0
- data/lib/generators/seams/admin/templates/lib/configuration.rb.tt +3 -1
- data/lib/generators/seams/admin/templates/spec/runtime/admin_boot_spec.rb.tt +4 -2
- data/lib/generators/seams/auth/add_oauth_provider/add_oauth_provider_generator.rb +1 -2
- data/lib/generators/seams/auth/auth_generator.rb +1 -2
- data/lib/generators/seams/auth/templates/app/controllers/oauth/callbacks_controller.rb.tt +3 -0
- data/lib/generators/seams/auth/templates/app/controllers/password_resets_controller.rb.tt +4 -0
- data/lib/generators/seams/auth/templates/app/controllers/registrations_controller.rb.tt +5 -0
- data/lib/generators/seams/auth/templates/app/controllers/sessions_controller.rb.tt +5 -0
- data/lib/generators/seams/auth/templates/db/migrate/create_auth_oauth_providers.rb.tt +5 -1
- data/lib/generators/seams/auth/templates/lib/concerns/authentication.rb.tt +12 -1
- data/lib/generators/seams/auth/templates/lib/engine.rb.tt +27 -0
- data/lib/generators/seams/billing/billing_generator.rb +4 -3
- data/lib/generators/seams/billing/templates/README.md.tt +26 -0
- data/lib/generators/seams/billing/templates/app/controllers/invoices_controller.rb.tt +2 -0
- data/lib/generators/seams/billing/templates/app/controllers/subscriptions_controller.rb.tt +2 -0
- data/lib/generators/seams/billing/templates/app/services/invoices/sync_service.rb.tt +8 -7
- data/lib/generators/seams/billing/templates/app/services/stripe_service.rb.tt +9 -7
- data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/invoice_handler_base.rb.tt +18 -3
- data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/subscription_handler_base.rb.tt +7 -21
- data/lib/generators/seams/billing/templates/lib/engine.rb.tt +15 -0
- data/lib/generators/seams/billing/templates/lib/gateways/stripe.rb.tt +10 -10
- data/lib/generators/seams/billing/templates/lib/stripe/client.rb.tt +40 -14
- data/lib/generators/seams/billing/templates/lib/stripe/payload.rb.tt +90 -0
- data/lib/generators/seams/billing/templates/lib/stripe/webhook_signature.rb.tt +11 -1
- data/lib/generators/seams/billing/templates/spec/fixtures/stripe/charge_refunded.json.tt +1 -0
- data/lib/generators/seams/billing/templates/spec/fixtures/stripe/checkout_session_completed.json.tt +1 -0
- data/lib/generators/seams/billing/templates/spec/fixtures/stripe/customer_subscription_created.json.tt +15 -4
- data/lib/generators/seams/billing/templates/spec/fixtures/stripe/customer_subscription_deleted.json.tt +7 -1
- data/lib/generators/seams/billing/templates/spec/fixtures/stripe/customer_subscription_trial_will_end.json.tt +18 -1
- data/lib/generators/seams/billing/templates/spec/fixtures/stripe/customer_subscription_updated.json.tt +26 -5
- data/lib/generators/seams/billing/templates/spec/fixtures/stripe/invoice_created.json.tt +16 -2
- data/lib/generators/seams/billing/templates/spec/fixtures/stripe/invoice_finalized.json.tt +16 -2
- data/lib/generators/seams/billing/templates/spec/fixtures/stripe/invoice_paid.json.tt +17 -2
- data/lib/generators/seams/billing/templates/spec/fixtures/stripe/invoice_payment_failed.json.tt +16 -2
- data/lib/generators/seams/billing/templates/spec/fixtures/stripe/invoice_voided.json.tt +16 -2
- data/lib/generators/seams/billing/templates/spec/fixtures/stripe/payment_intent_payment_failed.json.tt +1 -0
- data/lib/generators/seams/billing/templates/spec/fixtures/stripe/payment_intent_succeeded.json.tt +1 -0
- data/lib/generators/seams/billing/templates/spec/gateways/stripe_spec.rb.tt +47 -1
- data/lib/generators/seams/billing/templates/spec/runtime/webhook_handlers_spec.rb.tt +39 -1
- data/lib/generators/seams/design/design_generator.rb +647 -0
- data/lib/generators/seams/design/templates/README.md.tt +129 -0
- data/lib/generators/seams/design/templates/app/assets/tailwind/_tokens.css +322 -0
- data/lib/generators/seams/design/templates/app/assets/tailwind/themes/_quire.css +42 -0
- data/lib/generators/seams/design/templates/app/controllers/design/dashboard_controller.rb.tt +30 -0
- data/lib/generators/seams/design/templates/app/controllers/design/guide_controller.rb.tt +28 -0
- data/lib/generators/seams/design/templates/app/form_builders/design/form_builder.rb.tt +90 -0
- data/lib/generators/seams/design/templates/app/helpers/design/ui_helper.rb.tt +37 -0
- data/lib/generators/seams/design/templates/app/views/design/dashboard/index.html.erb.tt +38 -0
- data/lib/generators/seams/design/templates/app/views/design/guide/index.html.erb.tt +19 -0
- data/lib/generators/seams/design/templates/app/views/layouts/application.html.erb.tt +61 -0
- data/lib/generators/seams/design/templates/app/views/layouts/design/guide.html.erb.tt +21 -0
- data/lib/generators/seams/design/templates/app/views/ui/_banner.html.erb.tt +10 -0
- data/lib/generators/seams/design/templates/app/views/ui/_breadcrumb.html.erb.tt +8 -0
- data/lib/generators/seams/design/templates/app/views/ui/_build_row.html.erb.tt +14 -0
- data/lib/generators/seams/design/templates/app/views/ui/_button.html.erb.tt +4 -0
- data/lib/generators/seams/design/templates/app/views/ui/_card.html.erb.tt +2 -0
- data/lib/generators/seams/design/templates/app/views/ui/_chapter_row.html.erb.tt +11 -0
- data/lib/generators/seams/design/templates/app/views/ui/_checkbox.html.erb.tt +12 -0
- data/lib/generators/seams/design/templates/app/views/ui/_counter.html.erb.tt +2 -0
- data/lib/generators/seams/design/templates/app/views/ui/_data_table.html.erb.tt +25 -0
- data/lib/generators/seams/design/templates/app/views/ui/_dialog.html.erb.tt +14 -0
- data/lib/generators/seams/design/templates/app/views/ui/_diff.html.erb.tt +12 -0
- data/lib/generators/seams/design/templates/app/views/ui/_drawer.html.erb.tt +7 -0
- data/lib/generators/seams/design/templates/app/views/ui/_empty.html.erb.tt +5 -0
- data/lib/generators/seams/design/templates/app/views/ui/_field.html.erb.tt +14 -0
- data/lib/generators/seams/design/templates/app/views/ui/_icon.html.erb.tt +2 -0
- data/lib/generators/seams/design/templates/app/views/ui/_icon_sprite.html.erb.tt +22 -0
- data/lib/generators/seams/design/templates/app/views/ui/_input_group.html.erb.tt +11 -0
- data/lib/generators/seams/design/templates/app/views/ui/_kbd.html.erb.tt +4 -0
- data/lib/generators/seams/design/templates/app/views/ui/_menu.html.erb.tt +18 -0
- data/lib/generators/seams/design/templates/app/views/ui/_meter.html.erb.tt +15 -0
- data/lib/generators/seams/design/templates/app/views/ui/_note.html.erb.tt +2 -0
- data/lib/generators/seams/design/templates/app/views/ui/_outline.html.erb.tt +9 -0
- data/lib/generators/seams/design/templates/app/views/ui/_pagination.html.erb.tt +12 -0
- data/lib/generators/seams/design/templates/app/views/ui/_panel.html.erb.tt +2 -0
- data/lib/generators/seams/design/templates/app/views/ui/_popover.html.erb.tt +2 -0
- data/lib/generators/seams/design/templates/app/views/ui/_radio.html.erb.tt +5 -0
- data/lib/generators/seams/design/templates/app/views/ui/_savestate.html.erb.tt +5 -0
- data/lib/generators/seams/design/templates/app/views/ui/_segmented.html.erb.tt +7 -0
- data/lib/generators/seams/design/templates/app/views/ui/_stepper.html.erb.tt +12 -0
- data/lib/generators/seams/design/templates/app/views/ui/_switch.html.erb.tt +6 -0
- data/lib/generators/seams/design/templates/app/views/ui/_tag.html.erb.tt +2 -0
- data/lib/generators/seams/design/templates/app/views/ui/_toast.html.erb.tt +12 -0
- data/lib/generators/seams/design/templates/app/views/ui/_toolbar.html.erb.tt +15 -0
- data/lib/generators/seams/design/templates/app/views/ui/previews/_banner.html.erb.tt +8 -0
- data/lib/generators/seams/design/templates/app/views/ui/previews/_breadcrumb.html.erb.tt +5 -0
- data/lib/generators/seams/design/templates/app/views/ui/previews/_build_row.html.erb.tt +6 -0
- data/lib/generators/seams/design/templates/app/views/ui/previews/_button.html.erb.tt +9 -0
- data/lib/generators/seams/design/templates/app/views/ui/previews/_card.html.erb.tt +12 -0
- data/lib/generators/seams/design/templates/app/views/ui/previews/_chapter_row.html.erb.tt +6 -0
- data/lib/generators/seams/design/templates/app/views/ui/previews/_checkbox.html.erb.tt +6 -0
- data/lib/generators/seams/design/templates/app/views/ui/previews/_counter.html.erb.tt +6 -0
- data/lib/generators/seams/design/templates/app/views/ui/previews/_data_table.html.erb.tt +14 -0
- data/lib/generators/seams/design/templates/app/views/ui/previews/_dialog.html.erb.tt +8 -0
- data/lib/generators/seams/design/templates/app/views/ui/previews/_diff.html.erb.tt +10 -0
- data/lib/generators/seams/design/templates/app/views/ui/previews/_drawer.html.erb.tt +5 -0
- data/lib/generators/seams/design/templates/app/views/ui/previews/_empty.html.erb.tt +5 -0
- data/lib/generators/seams/design/templates/app/views/ui/previews/_field.html.erb.tt +5 -0
- data/lib/generators/seams/design/templates/app/views/ui/previews/_input_group.html.erb.tt +5 -0
- data/lib/generators/seams/design/templates/app/views/ui/previews/_kbd.html.erb.tt +5 -0
- data/lib/generators/seams/design/templates/app/views/ui/previews/_menu.html.erb.tt +9 -0
- data/lib/generators/seams/design/templates/app/views/ui/previews/_meter.html.erb.tt +6 -0
- data/lib/generators/seams/design/templates/app/views/ui/previews/_note.html.erb.tt +2 -0
- data/lib/generators/seams/design/templates/app/views/ui/previews/_outline.html.erb.tt +8 -0
- data/lib/generators/seams/design/templates/app/views/ui/previews/_pagination.html.erb.tt +5 -0
- data/lib/generators/seams/design/templates/app/views/ui/previews/_panel.html.erb.tt +5 -0
- data/lib/generators/seams/design/templates/app/views/ui/previews/_popover.html.erb.tt +2 -0
- data/lib/generators/seams/design/templates/app/views/ui/previews/_radio.html.erb.tt +6 -0
- data/lib/generators/seams/design/templates/app/views/ui/previews/_savestate.html.erb.tt +4 -0
- data/lib/generators/seams/design/templates/app/views/ui/previews/_segmented.html.erb.tt +15 -0
- data/lib/generators/seams/design/templates/app/views/ui/previews/_stepper.html.erb.tt +6 -0
- data/lib/generators/seams/design/templates/app/views/ui/previews/_switch.html.erb.tt +5 -0
- data/lib/generators/seams/design/templates/app/views/ui/previews/_tag.html.erb.tt +8 -0
- data/lib/generators/seams/design/templates/app/views/ui/previews/_toast.html.erb.tt +6 -0
- data/lib/generators/seams/design/templates/app/views/ui/previews/_toolbar.html.erb.tt +11 -0
- data/lib/generators/seams/design/templates/lib/design/components.rb.tt +22 -0
- data/lib/generators/seams/design/templates/lib/design.rb.tt +18 -0
- data/lib/generators/seams/design/templates/lib/engine.rb.tt +38 -0
- data/lib/generators/seams/design/templates/lib/generators/design/component/component_generator.rb.tt +52 -0
- data/lib/generators/seams/design/templates/lib/generators/design/component/templates/component.html.erb.tt +5 -0
- data/lib/generators/seams/design/templates/lib/generators/design/component/templates/preview.html.erb.tt +3 -0
- data/lib/generators/seams/design/templates/spec/runtime/design_boot_spec.rb.tt +79 -0
- data/lib/generators/seams/design/templates/spec/runtime/form_builder_spec.rb.tt +97 -0
- data/lib/generators/seams/design/templates/spec/runtime/guide_spec.rb.tt +40 -0
- data/lib/generators/seams/design/templates/spec/runtime/ui_components_spec.rb.tt +70 -0
- data/lib/generators/seams/engine/engine_generator.rb +6 -0
- data/lib/generators/seams/engine/templates/Gemfile.tt +1 -1
- data/lib/generators/seams/engine/templates/app/application_controller.rb.tt +6 -0
- data/lib/generators/seams/engine/templates/rubocop.yml.tt +9 -0
- data/lib/generators/seams/install/install_generator.rb +72 -6
- data/lib/generators/seams/install/templates/Dockerfile.tt +2 -2
- data/lib/generators/seams/install/templates/bin_seams.tt +7 -4
- data/lib/generators/seams/install/templates/ci.yml.tt +18 -8
- data/lib/generators/seams/install/templates/deploy.yml.tt +4 -2
- data/lib/generators/seams/install/templates/doc/ARCHITECTURE.md.tt +1 -1
- data/lib/generators/seams/install/templates/docker-entrypoint.tt +4 -2
- data/lib/generators/seams/install/templates/herb.yml.tt +10 -0
- data/lib/generators/seams/install/templates/lefthook.yml.tt +25 -0
- data/lib/generators/seams/install/templates/ruby-version.tt +1 -1
- data/lib/generators/seams/install/templates/seams.rake.tt +13 -1
- data/lib/generators/seams/install/templates/strong_migrations.rb.tt +26 -0
- data/lib/generators/seams/notifications/notifications_generator.rb +3 -2
- data/lib/generators/seams/notifications/templates/app/controllers/preferences_controller.rb.tt +5 -13
- data/lib/generators/seams/notifications/templates/lib/engine.rb.tt +12 -0
- data/lib/generators/seams/notifications/templates/lib/notifications.rb.tt +1 -0
- data/lib/generators/seams/notifications/templates/lib/preferences.rb.tt +36 -0
- data/lib/generators/seams/permissions/permissions_generator.rb +97 -0
- data/lib/generators/seams/permissions/templates/config/initializers/seams_permissions.rb.tt +35 -0
- data/lib/generators/seams/teams/templates/app/controllers/invitations_controller.rb.tt +27 -11
- data/lib/generators/seams/teams/templates/app/controllers/teams_controller.rb.tt +5 -13
- data/lib/generators/seams/teams/templates/lib/concerns/authorization.rb.tt +2 -4
- data/lib/generators/seams/teams/templates/lib/engine.rb.tt +13 -0
- data/lib/seams/cli/list.rb +15 -0
- data/lib/seams/cli/quality.rb +46 -5
- data/lib/seams/cli/resolve.rb +9 -5
- data/lib/seams/cli/test_changed.rb +5 -1
- data/lib/seams/cli.rb +20 -0
- data/lib/seams/configuration.rb +24 -1
- data/lib/seams/cops/no_cross_engine_dependency.rb +39 -7
- data/lib/seams/cops/no_cross_engine_model_access.rb +79 -1
- data/lib/seams/event_registry.rb +19 -0
- data/lib/seams/events/adapter.rb +9 -0
- data/lib/seams/events/adapters/active_support.rb +4 -0
- data/lib/seams/events/publisher.rb +29 -0
- data/lib/seams/events.rb +1 -0
- data/lib/seams/generators/dummy_app_writer.rb +21 -0
- data/lib/seams/generators/follow_up_generator.rb +1 -2
- data/lib/seams/generators/host_injector.rb +9 -2
- data/lib/seams/generators/splicer.rb +2 -2
- data/lib/seams/observability/adapter.rb +22 -0
- data/lib/seams/observability.rb +6 -0
- data/lib/seams/permission_registry.rb +66 -0
- data/lib/seams/permissions.rb +120 -0
- data/lib/seams/version.rb +1 -1
- data/lib/seams.rb +7 -0
- metadata +107 -3
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
require "seams"
|
|
5
|
+
require "seams/permissions"
|
|
6
|
+
require "seams/generators/host_injector"
|
|
7
|
+
require "seams/generators/eject_aware"
|
|
8
|
+
|
|
9
|
+
module Seams
|
|
10
|
+
module Generators
|
|
11
|
+
# Writes ONE host-editable initializer that spells out the default
|
|
12
|
+
# role -> ability grant map and assigns it through
|
|
13
|
+
# `Seams.configure { |c| c.permission_grants = {...} }`.
|
|
14
|
+
#
|
|
15
|
+
# This is the deferred-friendly seam of the permissions layer (Wave
|
|
16
|
+
# 11B / issue #37): there is no database table and no YAML DSL — a
|
|
17
|
+
# host changes who-can-do-what by editing this one Ruby file. The
|
|
18
|
+
# generated map is a readable copy of Seams::Permissions::DEFAULT_GRANTS
|
|
19
|
+
# rendered at generate time, so the host starts from the same defaults
|
|
20
|
+
# the gem ships and edits from there.
|
|
21
|
+
#
|
|
22
|
+
# The catalog of ability CODES is engine-owned: each engine registers
|
|
23
|
+
# the `resource.action.engine` codes it understands from its engine.rb
|
|
24
|
+
# (mirroring the event bus). This file only decides which ROLES hold
|
|
25
|
+
# which already-registered codes. Referencing a code no engine has
|
|
26
|
+
# registered makes `Seams::Permissions.can?` raise — deny-by-default,
|
|
27
|
+
# loudly.
|
|
28
|
+
#
|
|
29
|
+
# Run with: bin/seams permissions (or bin/rails generate seams:permissions)
|
|
30
|
+
class PermissionsGenerator < Rails::Generators::Base
|
|
31
|
+
include Seams::Generators::HostInjector
|
|
32
|
+
include Seams::Generators::EjectAware
|
|
33
|
+
|
|
34
|
+
source_root File.expand_path("templates", __dir__)
|
|
35
|
+
|
|
36
|
+
INITIALIZER_RELATIVE = "config/initializers/seams_permissions.rb"
|
|
37
|
+
|
|
38
|
+
# The single deliverable: the host-editable grant map. Eject-aware
|
|
39
|
+
# so a host that has stamped the eject header (to fully own the file
|
|
40
|
+
# and never be prompted again) keeps their version on a re-run.
|
|
41
|
+
def create_initializer
|
|
42
|
+
template_unless_ejected "config/initializers/seams_permissions.rb.tt",
|
|
43
|
+
host_path(INITIALIZER_RELATIVE)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def report_summary
|
|
47
|
+
say report_summary_text, :green
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def report_summary_text
|
|
51
|
+
<<~TXT
|
|
52
|
+
|
|
53
|
+
Permissions grant map generated at #{INITIALIZER_RELATIVE}
|
|
54
|
+
|
|
55
|
+
Next steps:
|
|
56
|
+
1. Edit the role -> ability map in
|
|
57
|
+
#{INITIALIZER_RELATIVE}
|
|
58
|
+
Each role lists the registered ability codes it holds. Roles
|
|
59
|
+
inherit downward (owner inherits admin inherits member), so a
|
|
60
|
+
code only needs to appear at the lowest role that should hold it.
|
|
61
|
+
|
|
62
|
+
2. List the ability codes every installed engine registers:
|
|
63
|
+
bin/rails runner 'pp Seams::PermissionRegistry.all'
|
|
64
|
+
You can only grant codes that appear there — `can?` raises on
|
|
65
|
+
an unregistered code (deny-by-default, loudly).
|
|
66
|
+
|
|
67
|
+
3. Guard a controller action with a code:
|
|
68
|
+
before_action -> { authorize_permission!("invoice.read.billing") }
|
|
69
|
+
|
|
70
|
+
To fully own this file (skip it on future generator runs):
|
|
71
|
+
bin/seams resolve --eject (or add the `# seams:ejected from` header).
|
|
72
|
+
|
|
73
|
+
See doc/reference/PERMISSIONS.md for the model, the role hierarchy, the bypass
|
|
74
|
+
tiers, and what is deliberately deferred (YAML DSL, DB custom roles,
|
|
75
|
+
per-ability grants).
|
|
76
|
+
|
|
77
|
+
TXT
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
# A readable Ruby literal for the default grant map, rendered from
|
|
83
|
+
# Seams::Permissions::DEFAULT_GRANTS so the generated file always
|
|
84
|
+
# matches the gem's shipped defaults. Each role becomes a
|
|
85
|
+
# `"role" => %w[...]` block (4-space indent), one ability code per
|
|
86
|
+
# line (6-space indent), so it sits cleanly inside the
|
|
87
|
+
# `config.permission_grants = {` hash in the template. Entries are
|
|
88
|
+
# joined with a trailing comma + newline.
|
|
89
|
+
def grant_map_body
|
|
90
|
+
Seams::Permissions::DEFAULT_GRANTS.map do |role, abilities|
|
|
91
|
+
codes = abilities.map { |code| " #{code}" }.join("\n")
|
|
92
|
+
%( "#{role}" => %w[\n#{codes}\n ])
|
|
93
|
+
end.join(",\n")
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Seams permissions — the host-editable role -> ability grant map.
|
|
4
|
+
#
|
|
5
|
+
# This is the ONE place you decide who-can-do-what. There is no database
|
|
6
|
+
# table and no DSL: authorisation is a code-defined role hierarchy plus
|
|
7
|
+
# this map. Edit the hash below, restart, done.
|
|
8
|
+
#
|
|
9
|
+
# How it resolves (Seams::Permissions.can?):
|
|
10
|
+
# - Roles inherit DOWNWARD: owner inherits admin inherits member. So a
|
|
11
|
+
# code only needs to appear at the LOWEST role that should hold it —
|
|
12
|
+
# anything granted to "member" is automatically held by admin + owner.
|
|
13
|
+
# - "system" is a pseudo-role that bypasses every check (background
|
|
14
|
+
# jobs, trusted internal actors). It needs no entry here.
|
|
15
|
+
# - Platform staff (Auth::Identity#staff?) are bypassed at the REQUEST
|
|
16
|
+
# layer by the caller (e.g. authorize_permission!), not here.
|
|
17
|
+
# - Deny-by-default: a role that is not granted a code does not hold it.
|
|
18
|
+
#
|
|
19
|
+
# The catalog of ability CODES is engine-owned. Each engine registers the
|
|
20
|
+
# `resource.action.engine` codes it understands from its engine.rb
|
|
21
|
+
# (mirroring the event bus). You can only grant codes that some engine has
|
|
22
|
+
# registered — referencing an unregistered code makes `can?` RAISE, not
|
|
23
|
+
# quietly allow. List the live catalog with:
|
|
24
|
+
#
|
|
25
|
+
# bin/rails runner 'pp Seams::PermissionRegistry.all'
|
|
26
|
+
#
|
|
27
|
+
# The map below is a copy of Seams::Permissions::DEFAULT_GRANTS as shipped
|
|
28
|
+
# by the gem at the time this file was generated. Add, remove, or move
|
|
29
|
+
# codes between roles to fit your product. To add a brand-new code, first
|
|
30
|
+
# register it from the owning engine's engine.rb, then grant it here.
|
|
31
|
+
Seams.configure do |config|
|
|
32
|
+
config.permission_grants = {
|
|
33
|
+
<%= grant_map_body %>
|
|
34
|
+
}
|
|
35
|
+
end
|
|
@@ -41,7 +41,18 @@ module Teams
|
|
|
41
41
|
# Wraps the lookup in a row lock and short-circuits if the
|
|
42
42
|
# invitation has already been accepted, so a double-click on the
|
|
43
43
|
# email link returns a friendly redirect instead of a 500.
|
|
44
|
+
#
|
|
45
|
+
# Unauthenticated requests stash the token in the session and
|
|
46
|
+
# redirect to the sign-in path with a `return_to` parameter equal
|
|
47
|
+
# to the current request URL. The host's sign-in flow is responsible
|
|
48
|
+
# for reading `return_to` and redirecting the user back after
|
|
49
|
+
# authentication.
|
|
44
50
|
def accept
|
|
51
|
+
unless current_identity_id
|
|
52
|
+
session[:pending_invitation_token] = params[:token]
|
|
53
|
+
return redirect_to(main_app.new_session_path(return_to: request.url))
|
|
54
|
+
end
|
|
55
|
+
|
|
45
56
|
Teams::Invitation.transaction do
|
|
46
57
|
@invitation = Teams::Invitation.lock.find_by!(token: params[:token])
|
|
47
58
|
|
|
@@ -53,6 +64,18 @@ module Teams
|
|
|
53
64
|
return redirect_to root_path, alert: "Invitation expired"
|
|
54
65
|
end
|
|
55
66
|
|
|
67
|
+
# Prevent a holder of a stolen token from accepting the
|
|
68
|
+
# invitation under a different identity. Compare
|
|
69
|
+
# case-insensitively to guard against trivial casing bypasses.
|
|
70
|
+
current_email = if defined?(Auth::Current) && Auth::Current.respond_to?(:identity) && Auth::Current.identity
|
|
71
|
+
Auth::Current.identity.email.to_s.downcase
|
|
72
|
+
else
|
|
73
|
+
""
|
|
74
|
+
end
|
|
75
|
+
unless current_email == @invitation.email.to_s.downcase
|
|
76
|
+
return redirect_to teams_path, alert: "This invitation was sent to a different email address."
|
|
77
|
+
end
|
|
78
|
+
|
|
56
79
|
@invitation.team.memberships.create!(identity_id: current_identity_id, role: @invitation.role)
|
|
57
80
|
@invitation.update!(accepted_at: Time.current)
|
|
58
81
|
end
|
|
@@ -87,16 +110,9 @@ module Teams
|
|
|
87
110
|
Teams::Membership::ROLES.include?(candidate) ? candidate : "member"
|
|
88
111
|
end
|
|
89
112
|
|
|
90
|
-
#
|
|
91
|
-
#
|
|
92
|
-
#
|
|
93
|
-
#
|
|
94
|
-
def current_identity_id
|
|
95
|
-
if defined?(Auth::Current) && Auth::Current.respond_to?(:identity) && Auth::Current.identity
|
|
96
|
-
return Auth::Current.identity.id
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
nil
|
|
100
|
-
end
|
|
113
|
+
# current_identity_id is provided by Teams::Authorization (included
|
|
114
|
+
# at the top of the class). Do not redefine here — keep the concern
|
|
115
|
+
# as the single source of truth so a future change to identity
|
|
116
|
+
# resolution lands in one place.
|
|
101
117
|
end
|
|
102
118
|
end
|
|
@@ -2,7 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
module Teams
|
|
4
4
|
class TeamsController < ApplicationController
|
|
5
|
-
|
|
5
|
+
include Teams::Authorization
|
|
6
|
+
|
|
7
|
+
before_action :set_team, only: %i[show edit update destroy]
|
|
8
|
+
before_action :require_team_member!, only: %i[show]
|
|
9
|
+
before_action :require_team_admin!, only: %i[edit update destroy]
|
|
6
10
|
|
|
7
11
|
def index
|
|
8
12
|
@teams = Teams::Team.joins(:memberships).where(memberships: { identity_id: current_identity_id })
|
|
@@ -52,17 +56,5 @@ module Teams
|
|
|
52
56
|
def team_params
|
|
53
57
|
params.require(:team).permit(:name, :slug)
|
|
54
58
|
end
|
|
55
|
-
|
|
56
|
-
# Resolves the signed-in human's id from `Auth::Current.identity`
|
|
57
|
-
# (the Auth engine's per-request namespace). Gated on
|
|
58
|
-
# `defined?(Auth::Current)` so it's safe in hosts that don't ship
|
|
59
|
-
# auth. Override in your host if you wire auth differently.
|
|
60
|
-
def current_identity_id
|
|
61
|
-
if defined?(Auth::Current) && Auth::Current.respond_to?(:identity) && Auth::Current.identity
|
|
62
|
-
return Auth::Current.identity.id
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
nil
|
|
66
|
-
end
|
|
67
59
|
end
|
|
68
60
|
end
|
|
@@ -18,13 +18,11 @@ module Teams
|
|
|
18
18
|
private
|
|
19
19
|
|
|
20
20
|
def require_team_member!
|
|
21
|
-
|
|
22
|
-
true
|
|
21
|
+
redirect_to teams_path, alert: "Not a member of that team." unless team_member?
|
|
23
22
|
end
|
|
24
23
|
|
|
25
24
|
def require_team_admin!
|
|
26
|
-
|
|
27
|
-
true
|
|
25
|
+
redirect_to teams_path, alert: "Admin access required." unless team_admin?
|
|
28
26
|
end
|
|
29
27
|
|
|
30
28
|
def team_member?
|
|
@@ -18,6 +18,19 @@ module Teams
|
|
|
18
18
|
# seams:insertion-point teams.engine.events
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
+
initializer "teams.register_abilities" do
|
|
22
|
+
# Ability codes this engine owns, registered exactly like events
|
|
23
|
+
# (resource.action.engine). `read` is the member-level view;
|
|
24
|
+
# `manage` is the admin-level mutate. Host grant maps reference
|
|
25
|
+
# these codes; see Seams::Permissions::DEFAULT_GRANTS.
|
|
26
|
+
Seams::PermissionRegistry.register("team.read.teams", owned_by: "Teams")
|
|
27
|
+
Seams::PermissionRegistry.register("team.manage.teams", owned_by: "Teams")
|
|
28
|
+
Seams::PermissionRegistry.register("member.manage.teams", owned_by: "Teams")
|
|
29
|
+
Seams::PermissionRegistry.register("invitation.manage.teams", owned_by: "Teams")
|
|
30
|
+
# Follow-up generators that ship new teams abilities register them here.
|
|
31
|
+
# seams:insertion-point teams.engine.abilities
|
|
32
|
+
end
|
|
33
|
+
|
|
21
34
|
initializer "teams.append_migrations" do |app|
|
|
22
35
|
unless app.root == root
|
|
23
36
|
config.paths["db/migrate"].expanded.each do |expanded_path|
|
data/lib/seams/cli/list.rb
CHANGED
|
@@ -8,15 +8,29 @@ module Seams
|
|
|
8
8
|
# engine under engines/, looks up its registered events in
|
|
9
9
|
# Seams::EventRegistry, and prints a summary.
|
|
10
10
|
class List
|
|
11
|
+
# Default directory that holds the generated engines.
|
|
11
12
|
DEFAULT_ENGINES_ROOT = "engines"
|
|
12
13
|
|
|
14
|
+
# Matches the top-level `module Foo` declaration in an engine file.
|
|
13
15
|
MODULE_DECLARATION = /\bmodule\s+([A-Z][A-Za-z0-9_]*)\b/
|
|
14
16
|
|
|
17
|
+
# The canonical event-name convention: at least three
|
|
18
|
+
# dot-separated word segments ("subscription.created.billing").
|
|
19
|
+
# Used to reject regex captures that are NOT event names — the
|
|
20
|
+
# subscription scan is textual and multiline, so when an event
|
|
21
|
+
# name is passed as a variable it would otherwise grab the next
|
|
22
|
+
# unrelated string literal downstream (e.g. a subscriber's
|
|
23
|
+
# owner-class fallback like "Accounts::Account") and report a
|
|
24
|
+
# phantom subscription/dependency.
|
|
25
|
+
EVENT_NAME = /\A\w+(\.\w+){2,}\z/
|
|
26
|
+
|
|
15
27
|
def initialize(engines_root: DEFAULT_ENGINES_ROOT, output: $stdout)
|
|
16
28
|
@engines_root = engines_root
|
|
17
29
|
@output = output
|
|
18
30
|
end
|
|
19
31
|
|
|
32
|
+
# Discover the engines, gather their events, and print the report.
|
|
33
|
+
# @return [Boolean] true on success.
|
|
20
34
|
def call
|
|
21
35
|
engines = discover_engines
|
|
22
36
|
@output.puts("seams: #{engines.size} engine(s) installed")
|
|
@@ -69,6 +83,7 @@ module Seams
|
|
|
69
83
|
content.scan(/Publisher\.(?:subscribe|attach_once|attach_class)\([^"']*["']([^"']+)["']/m)
|
|
70
84
|
.flatten
|
|
71
85
|
end
|
|
86
|
+
.grep(EVENT_NAME)
|
|
72
87
|
.uniq
|
|
73
88
|
end
|
|
74
89
|
|
data/lib/seams/cli/quality.rb
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "open3"
|
|
3
4
|
require "seams"
|
|
4
5
|
|
|
5
6
|
module Seams
|
|
@@ -14,7 +15,10 @@ module Seams
|
|
|
14
15
|
#
|
|
15
16
|
# Returns true when every gate that ran passed; false otherwise.
|
|
16
17
|
class Quality
|
|
18
|
+
# Default directory that holds the generated engines.
|
|
17
19
|
DEFAULT_ENGINES_ROOT = "engines"
|
|
20
|
+
# Gate statuses that count as a pass for the overall result.
|
|
21
|
+
PASSING_STATUSES = %i[pass skipped].freeze
|
|
18
22
|
|
|
19
23
|
def initialize(engines_root: DEFAULT_ENGINES_ROOT, output: $stdout)
|
|
20
24
|
@engines_root = engines_root
|
|
@@ -22,6 +26,8 @@ module Seams
|
|
|
22
26
|
@results = {}
|
|
23
27
|
end
|
|
24
28
|
|
|
29
|
+
# Run the quality gates across every engine.
|
|
30
|
+
# @return [Boolean] true when every gate that ran passed.
|
|
25
31
|
def call
|
|
26
32
|
run_rubocop
|
|
27
33
|
run_brakeman
|
|
@@ -29,19 +35,54 @@ module Seams
|
|
|
29
35
|
run_simplecov_collation
|
|
30
36
|
|
|
31
37
|
print_summary
|
|
32
|
-
@results.values.all? { |status|
|
|
38
|
+
@results.values.all? { |status| PASSING_STATUSES.include?(status) }
|
|
33
39
|
end
|
|
34
40
|
|
|
35
41
|
private
|
|
36
42
|
|
|
43
|
+
# Runs RuboCop twice over: once host-wide, then once per engine.
|
|
44
|
+
#
|
|
45
|
+
# The per-engine runs are NOT redundant: the generated host
|
|
46
|
+
# .rubocop.yml excludes `engines/**/*` (engines lint themselves),
|
|
47
|
+
# and RuboCop merges the project root's `AllCops: Exclude` into
|
|
48
|
+
# any run started from a subdirectory unless
|
|
49
|
+
# `--ignore-parent-exclusion` is passed. A single host-wide run
|
|
50
|
+
# therefore inspects ZERO engine files, silently skipping the
|
|
51
|
+
# Seams boundary cops. Each engine with its own .rubocop.yml is
|
|
52
|
+
# linted from inside its directory with parent exclusion ignored.
|
|
37
53
|
def run_rubocop
|
|
38
|
-
|
|
39
|
-
@output.puts("=== rubocop --parallel ===")
|
|
40
|
-
@results[:rubocop] = system("bundle", "exec", "rubocop", "--parallel") ? :pass : :fail
|
|
41
|
-
else
|
|
54
|
+
unless gem_installed?("rubocop")
|
|
42
55
|
@output.puts("rubocop not installed — skipping.")
|
|
43
56
|
@results[:rubocop] = :skipped
|
|
57
|
+
return
|
|
44
58
|
end
|
|
59
|
+
|
|
60
|
+
@output.puts("=== rubocop --parallel ===")
|
|
61
|
+
host_ok = system("bundle", "exec", "rubocop", "--parallel")
|
|
62
|
+
|
|
63
|
+
# Run every engine even after a failure so the summary shows
|
|
64
|
+
# all offending engines, not just the first.
|
|
65
|
+
engines_ok = engine_rubocop_dirs.map { |dir| engine_rubocop_passed?(dir) }.all?
|
|
66
|
+
|
|
67
|
+
@results[:rubocop] = host_ok && engines_ok ? :pass : :fail
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def engine_rubocop_passed?(dir)
|
|
71
|
+
@output.puts("=== rubocop --ignore-parent-exclusion (#{dir}) ===")
|
|
72
|
+
out, status = Open3.capture2e(
|
|
73
|
+
"bundle", "exec", "rubocop", "--ignore-parent-exclusion", chdir: dir
|
|
74
|
+
)
|
|
75
|
+
@output.puts(out)
|
|
76
|
+
status.success?
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Engines that ship their own .rubocop.yml (the generated
|
|
80
|
+
# default). Engines without one fall back to whatever the host
|
|
81
|
+
# run covers, so they are skipped here.
|
|
82
|
+
def engine_rubocop_dirs
|
|
83
|
+
Dir.glob(File.join(@engines_root, "*"))
|
|
84
|
+
.select { |dir| File.directory?(dir) && File.exist?(File.join(dir, ".rubocop.yml")) }
|
|
85
|
+
.sort
|
|
45
86
|
end
|
|
46
87
|
|
|
47
88
|
def run_brakeman
|
data/lib/seams/cli/resolve.rb
CHANGED
|
@@ -41,16 +41,19 @@ module Seams
|
|
|
41
41
|
# with a trailing `?`, but that's wrong for the run_* dispatchers
|
|
42
42
|
# (they're imperative, not predicates). AbcSize / CyclomaticComplexity
|
|
43
43
|
# likewise trigger on the run_* methods because CLI command
|
|
44
|
-
# branches are inherently branchy. The cops are disabled
|
|
45
|
-
#
|
|
46
|
-
# rubocop:disable Naming/PredicateMethod, Metrics/AbcSize, Metrics/CyclomaticComplexity
|
|
44
|
+
# branches are inherently branchy. The cops are disabled for the
|
|
45
|
+
# whole class and the methods are kept linear and well-commented.
|
|
46
|
+
# rubocop:disable-next Naming/PredicateMethod, Metrics/AbcSize, Metrics/CyclomaticComplexity
|
|
47
47
|
class Resolve
|
|
48
|
+
# Default directory that holds the generated engines.
|
|
48
49
|
DEFAULT_ENGINES_ROOT = "engines"
|
|
49
50
|
|
|
50
51
|
# Header injected at the top of every ejected file. Position
|
|
51
52
|
# matters: future regenerations check the FIRST line of an
|
|
52
53
|
# existing destination file for this exact prefix.
|
|
53
54
|
EJECT_HEADER_PREFIX = "# seams:ejected from"
|
|
55
|
+
|
|
56
|
+
# Builds the full ejected-file header block for a given event/source.
|
|
54
57
|
EJECT_HEADER_LINES = lambda do |from|
|
|
55
58
|
<<~HEADER
|
|
56
59
|
#{EJECT_HEADER_PREFIX} #{from}
|
|
@@ -78,6 +81,8 @@ module Seams
|
|
|
78
81
|
@error = error
|
|
79
82
|
end
|
|
80
83
|
|
|
84
|
+
# Execute the selected resolve mode (:eject / :list_markers / :list_ejected).
|
|
85
|
+
# @return [Boolean] true on success, false on a handled failure.
|
|
81
86
|
def call
|
|
82
87
|
case @mode
|
|
83
88
|
when :eject then run_eject
|
|
@@ -138,7 +143,7 @@ module Seams
|
|
|
138
143
|
fail_with(
|
|
139
144
|
"refusing to eject #{engine}/#{relative}: this file is framework-managed " \
|
|
140
145
|
"(migrations, engine.rb, version.rb, Gemfile, .gemspec) and is not eject-eligible. " \
|
|
141
|
-
"See doc/INSERTION_POINTS.md and Seams::Generators::EjectAware for the rule."
|
|
146
|
+
"See doc/reference/INSERTION_POINTS.md and Seams::Generators::EjectAware for the rule."
|
|
142
147
|
)
|
|
143
148
|
false
|
|
144
149
|
end
|
|
@@ -271,6 +276,5 @@ module Seams
|
|
|
271
276
|
false
|
|
272
277
|
end
|
|
273
278
|
end
|
|
274
|
-
# rubocop:enable Naming/PredicateMethod, Metrics/AbcSize, Metrics/CyclomaticComplexity
|
|
275
279
|
end
|
|
276
280
|
end
|
|
@@ -17,7 +17,9 @@ module Seams
|
|
|
17
17
|
# The caller (rake task or bin/seams) translates that to an exit
|
|
18
18
|
# code.
|
|
19
19
|
class TestChanged
|
|
20
|
+
# Default git ref to diff the working tree against.
|
|
20
21
|
DEFAULT_BASE = "main"
|
|
22
|
+
# Default directory that holds the generated engines.
|
|
21
23
|
DEFAULT_ENGINES_ROOT = "engines"
|
|
22
24
|
|
|
23
25
|
def initialize(base: DEFAULT_BASE, engines_root: DEFAULT_ENGINES_ROOT, output: $stdout)
|
|
@@ -26,6 +28,8 @@ module Seams
|
|
|
26
28
|
@output = output
|
|
27
29
|
end
|
|
28
30
|
|
|
31
|
+
# Run the specs of every engine changed since {DEFAULT_BASE}.
|
|
32
|
+
# @return [Boolean] true if all selected suites pass.
|
|
29
33
|
def call
|
|
30
34
|
engines = changed_engines
|
|
31
35
|
if engines.empty?
|
|
@@ -59,7 +63,7 @@ module Seams
|
|
|
59
63
|
end
|
|
60
64
|
|
|
61
65
|
# Array-form shell-out: no interpolation into a shell, so a
|
|
62
|
-
#
|
|
66
|
+
# +@base+ value like `; rm -rf /` passes as one argv element to
|
|
63
67
|
# git rather than getting evaluated. shell_escape is defence in
|
|
64
68
|
# depth on top of that.
|
|
65
69
|
def resolve_merge_base
|
data/lib/seams/cli.rb
CHANGED
|
@@ -19,10 +19,19 @@ module Seams
|
|
|
19
19
|
module CLI
|
|
20
20
|
module_function
|
|
21
21
|
|
|
22
|
+
# List every engine and the events it emits/subscribes to.
|
|
23
|
+
# @param engines_root [String] directory holding the engines.
|
|
24
|
+
# @param output [IO] stream to write the report to.
|
|
25
|
+
# @return [Boolean] true on success.
|
|
22
26
|
def list(engines_root: "engines", output: $stdout)
|
|
23
27
|
Seams::CLI::List.new(engines_root: engines_root, output: output).call
|
|
24
28
|
end
|
|
25
29
|
|
|
30
|
+
# Run the specs of every engine changed since +base+.
|
|
31
|
+
# @param base [String] the git ref to diff against.
|
|
32
|
+
# @param engines_root [String] directory holding the engines.
|
|
33
|
+
# @param output [IO] stream to write progress to.
|
|
34
|
+
# @return [Boolean] true if all selected suites pass.
|
|
26
35
|
def test_changed(base: "main", engines_root: "engines", output: $stdout)
|
|
27
36
|
Seams::CLI::TestChanged.new(
|
|
28
37
|
base: base,
|
|
@@ -31,10 +40,21 @@ module Seams
|
|
|
31
40
|
).call
|
|
32
41
|
end
|
|
33
42
|
|
|
43
|
+
# Run rubocop across every engine.
|
|
44
|
+
# @param engines_root [String] directory holding the engines.
|
|
45
|
+
# @param output [IO] stream to write progress to.
|
|
46
|
+
# @return [Boolean] true if every engine lints clean.
|
|
34
47
|
def quality(engines_root: "engines", output: $stdout)
|
|
35
48
|
Seams::CLI::Quality.new(engines_root: engines_root, output: output).call
|
|
36
49
|
end
|
|
37
50
|
|
|
51
|
+
# Drive the eject / insertion-point escape hatch.
|
|
52
|
+
# @param mode [Symbol] :eject, :list_markers, or :list_ejected.
|
|
53
|
+
# @param argument [String, nil] mode-specific argument (e.g. "<engine>/<file>").
|
|
54
|
+
# @param engines_root [String] directory holding the engines.
|
|
55
|
+
# @param output [IO] stream for normal output.
|
|
56
|
+
# @param error [IO] stream for error output.
|
|
57
|
+
# @return [Boolean] true on success.
|
|
38
58
|
def resolve(mode:, argument: nil, engines_root: "engines", output: $stdout, error: $stderr)
|
|
39
59
|
Seams::CLI::Resolve.new(
|
|
40
60
|
mode: mode,
|
data/lib/seams/configuration.rb
CHANGED
|
@@ -1,19 +1,42 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "seams/permissions"
|
|
4
|
+
|
|
3
5
|
module Seams
|
|
4
6
|
# Global Seams configuration. Set via Seams.configure { |c| ... } in
|
|
5
7
|
# config/initializers/seams.rb of the host application.
|
|
8
|
+
#
|
|
9
|
+
# @example
|
|
10
|
+
# Seams.configure do |c|
|
|
11
|
+
# c.host_app_name = "Acme"
|
|
12
|
+
# c.event_bus_adapter = "MyApp::Events::SidekiqAdapter"
|
|
13
|
+
# end
|
|
6
14
|
class Configuration
|
|
15
|
+
# @!attribute [rw] event_bus_adapter
|
|
16
|
+
# @return [String] class name of the event-bus adapter (default
|
|
17
|
+
# "Seams::Events::Adapters::ActiveSupport").
|
|
18
|
+
# @!attribute [rw] observability_adapter
|
|
19
|
+
# @return [String] class name of the observability adapter (default
|
|
20
|
+
# "Seams::Observability::Adapters::RailsLogger").
|
|
21
|
+
# @!attribute [rw] event_namespace_separator
|
|
22
|
+
# @return [String] separator between event-name segments (default ".").
|
|
23
|
+
# @!attribute [rw] host_app_name
|
|
24
|
+
# @return [String, nil] the host application's name, for reporting.
|
|
25
|
+
# @!attribute [rw] permission_grants
|
|
26
|
+
# @return [Hash] the role -> ability grant map (default
|
|
27
|
+
# {Seams::Permissions::DEFAULT_GRANTS}).
|
|
7
28
|
attr_accessor :event_bus_adapter,
|
|
8
29
|
:observability_adapter,
|
|
9
30
|
:event_namespace_separator,
|
|
10
|
-
:host_app_name
|
|
31
|
+
:host_app_name,
|
|
32
|
+
:permission_grants
|
|
11
33
|
|
|
12
34
|
def initialize
|
|
13
35
|
@event_bus_adapter = "Seams::Events::Adapters::ActiveSupport"
|
|
14
36
|
@observability_adapter = "Seams::Observability::Adapters::RailsLogger"
|
|
15
37
|
@event_namespace_separator = "."
|
|
16
38
|
@host_app_name = nil
|
|
39
|
+
@permission_grants = Seams::Permissions::DEFAULT_GRANTS
|
|
17
40
|
end
|
|
18
41
|
end
|
|
19
42
|
end
|
|
@@ -10,19 +10,22 @@ module RuboCop
|
|
|
10
10
|
# through public events (Seams::Events::Publisher) and
|
|
11
11
|
# explicitly-exposed concerns — not by requiring private files.
|
|
12
12
|
class NoCrossEngineDependency < Base
|
|
13
|
+
# Path segments that climb (or stay in) the directory tree.
|
|
14
|
+
CLIMB_SEGMENTS = [".", ".."].freeze
|
|
15
|
+
|
|
13
16
|
MSG = "Engine `%<own>s` must not require `%<path>s` from another engine. " \
|
|
14
17
|
"Communicate via events or via `%<other>s`'s exposed concerns."
|
|
15
18
|
|
|
16
19
|
# @!method require_call?(node)
|
|
17
20
|
def_node_matcher :require_call?, <<~PATTERN
|
|
18
|
-
(send nil? {:require :require_relative} (str $_))
|
|
21
|
+
(send nil? ${:require :require_relative} (str $_))
|
|
19
22
|
PATTERN
|
|
20
23
|
|
|
21
24
|
def on_send(node)
|
|
22
|
-
path = require_call?(node)
|
|
25
|
+
method, path = require_call?(node)
|
|
23
26
|
return unless path
|
|
24
27
|
|
|
25
|
-
offending_engine = other_engine_for(path)
|
|
28
|
+
offending_engine = other_engine_for(method, path)
|
|
26
29
|
return unless offending_engine
|
|
27
30
|
|
|
28
31
|
add_offense(
|
|
@@ -42,11 +45,40 @@ module RuboCop
|
|
|
42
45
|
Array(cop_config["OtherEngines"]).map(&:to_s)
|
|
43
46
|
end
|
|
44
47
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
+
# For a plain `require`, only the FIRST path segment identifies
|
|
49
|
+
# the library — `require "billing/foo"` loads the sibling engine,
|
|
50
|
+
# but `require "rspec/core/rake_task"` loads rspec-core no matter
|
|
51
|
+
# what its deeper segments are called. Matching ANY segment (the
|
|
52
|
+
# old behaviour) false-fired on every generated host with an
|
|
53
|
+
# engine named `core`, because engine Rakefiles require
|
|
54
|
+
# "rspec/core/rake_task".
|
|
55
|
+
#
|
|
56
|
+
# For `require_relative`, the path is anchored inside the
|
|
57
|
+
# engine's own tree: it only reaches a sibling engine when it
|
|
58
|
+
# first climbs OUT via `../`. Resolve the climb and check the
|
|
59
|
+
# first segment after it — `../../billing/foo` is a genuine
|
|
60
|
+
# cross-engine reach, while `core/something` (no climb) is just
|
|
61
|
+
# a subdirectory of the requiring file.
|
|
62
|
+
def other_engine_for(method, path)
|
|
63
|
+
candidate =
|
|
64
|
+
if method == :require_relative
|
|
65
|
+
first_segment_after_climb(path)
|
|
66
|
+
else
|
|
67
|
+
path.split("/").first
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
other_engines.find { |engine| engine == candidate }
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Returns the first path segment after the leading `../` (or
|
|
74
|
+
# `./`) climb, or nil when the path never leaves the requiring
|
|
75
|
+
# file's own directory.
|
|
76
|
+
def first_segment_after_climb(path)
|
|
48
77
|
segments = path.split("/")
|
|
49
|
-
|
|
78
|
+
climbed = segments.take_while { |segment| CLIMB_SEGMENTS.include?(segment) }
|
|
79
|
+
return nil unless climbed.include?("..")
|
|
80
|
+
|
|
81
|
+
segments.drop(climbed.size).first
|
|
50
82
|
end
|
|
51
83
|
|
|
52
84
|
def capitalize(name)
|