seams 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/CHANGELOG.md +335 -0
- data/LICENSE +21 -0
- data/README.md +104 -0
- data/lib/generators/seams/accounts/accounts_generator.rb +272 -0
- data/lib/generators/seams/accounts/templates/README.md.tt +219 -0
- data/lib/generators/seams/accounts/templates/app/models/account.rb.tt +124 -0
- data/lib/generators/seams/accounts/templates/app/models/application_record.rb.tt +7 -0
- data/lib/generators/seams/accounts/templates/app/models/current.rb.tt +38 -0
- data/lib/generators/seams/accounts/templates/app/models/membership.rb.tt +114 -0
- data/lib/generators/seams/accounts/templates/config/routes.rb.tt +8 -0
- data/lib/generators/seams/accounts/templates/db/migrate/create_accounts.rb.tt +29 -0
- data/lib/generators/seams/accounts/templates/db/migrate/create_accounts_memberships.rb.tt +49 -0
- data/lib/generators/seams/accounts/templates/lib/accounts.rb.tt +21 -0
- data/lib/generators/seams/accounts/templates/lib/concerns/account_scoped.rb.tt +97 -0
- data/lib/generators/seams/accounts/templates/lib/concerns/authorization.rb.tt +105 -0
- data/lib/generators/seams/accounts/templates/lib/configuration.rb.tt +26 -0
- data/lib/generators/seams/accounts/templates/lib/engine.rb.tt +55 -0
- data/lib/generators/seams/accounts/templates/spec/factories/accounts.rb.tt +49 -0
- data/lib/generators/seams/accounts/templates/spec/models/accounts/account_spec.rb.tt +64 -0
- data/lib/generators/seams/accounts/templates/spec/models/accounts/membership_spec.rb.tt +99 -0
- data/lib/generators/seams/accounts/templates/spec/runtime/accounts_boot_spec.rb.tt +181 -0
- data/lib/generators/seams/admin/admin_generator.rb +852 -0
- data/lib/generators/seams/admin/templates/README.md.tt +266 -0
- data/lib/generators/seams/admin/templates/app/controllers/admin/accounts_controller.rb.tt +16 -0
- data/lib/generators/seams/admin/templates/app/controllers/admin/accounts_memberships_controller.rb.tt +16 -0
- data/lib/generators/seams/admin/templates/app/controllers/admin/application_controller.rb.tt +282 -0
- data/lib/generators/seams/admin/templates/app/controllers/admin/identities_controller.rb.tt +26 -0
- data/lib/generators/seams/admin/templates/app/controllers/admin/invitations_controller.rb.tt +14 -0
- data/lib/generators/seams/admin/templates/app/controllers/admin/invoices_controller.rb.tt +14 -0
- data/lib/generators/seams/admin/templates/app/controllers/admin/lifetime_passes_controller.rb.tt +14 -0
- data/lib/generators/seams/admin/templates/app/controllers/admin/notification_preferences_controller.rb.tt +15 -0
- data/lib/generators/seams/admin/templates/app/controllers/admin/notifications_controller.rb.tt +18 -0
- data/lib/generators/seams/admin/templates/app/controllers/admin/plans_controller.rb.tt +14 -0
- data/lib/generators/seams/admin/templates/app/controllers/admin/subscriptions_controller.rb.tt +14 -0
- data/lib/generators/seams/admin/templates/app/controllers/admin/teams_controller.rb.tt +14 -0
- data/lib/generators/seams/admin/templates/app/controllers/admin/teams_memberships_controller.rb.tt +16 -0
- data/lib/generators/seams/admin/templates/app/dashboards/admin/account_dashboard.rb.tt +50 -0
- data/lib/generators/seams/admin/templates/app/dashboards/admin/accounts_membership_dashboard.rb.tt +58 -0
- data/lib/generators/seams/admin/templates/app/dashboards/admin/identity_dashboard.rb.tt +48 -0
- data/lib/generators/seams/admin/templates/app/dashboards/admin/invitation_dashboard.rb.tt +51 -0
- data/lib/generators/seams/admin/templates/app/dashboards/admin/invoice_dashboard.rb.tt +67 -0
- data/lib/generators/seams/admin/templates/app/dashboards/admin/lifetime_pass_dashboard.rb.tt +65 -0
- data/lib/generators/seams/admin/templates/app/dashboards/admin/notification_dashboard.rb.tt +58 -0
- data/lib/generators/seams/admin/templates/app/dashboards/admin/notification_preference_dashboard.rb.tt +43 -0
- data/lib/generators/seams/admin/templates/app/dashboards/admin/plan_dashboard.rb.tt +72 -0
- data/lib/generators/seams/admin/templates/app/dashboards/admin/subscription_dashboard.rb.tt +59 -0
- data/lib/generators/seams/admin/templates/app/dashboards/admin/team_dashboard.rb.tt +39 -0
- data/lib/generators/seams/admin/templates/app/dashboards/admin/teams_membership_dashboard.rb.tt +43 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/platform/account_policy.rb.tt +10 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/platform/accounts_membership_policy.rb.tt +10 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/platform/application_policy.rb.tt +85 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/platform/identity_policy.rb.tt +18 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/platform/invitation_policy.rb.tt +9 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/platform/invoice_policy.rb.tt +9 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/platform/lifetime_pass_policy.rb.tt +9 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/platform/notification_policy.rb.tt +9 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/platform/notification_preference_policy.rb.tt +9 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/platform/plan_policy.rb.tt +11 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/platform/subscription_policy.rb.tt +9 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/platform/team_policy.rb.tt +9 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/platform/teams_membership_policy.rb.tt +9 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/tenant/account_policy.rb.tt +33 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/tenant/accounts_membership_policy.rb.tt +24 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/tenant/application_policy.rb.tt +169 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/tenant/identity_policy.rb.tt +67 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/tenant/invitation_policy.rb.tt +24 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/tenant/invoice_policy.rb.tt +21 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/tenant/lifetime_pass_policy.rb.tt +21 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/tenant/notification_policy.rb.tt +25 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/tenant/notification_preference_policy.rb.tt +23 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/tenant/plan_policy.rb.tt +47 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/tenant/subscription_policy.rb.tt +22 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/tenant/team_policy.rb.tt +28 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/tenant/teams_membership_policy.rb.tt +24 -0
- data/lib/generators/seams/admin/templates/config/routes.rb.tt +38 -0
- data/lib/generators/seams/admin/templates/lib/admin.rb.tt +36 -0
- data/lib/generators/seams/admin/templates/lib/concerns/authenticator.rb.tt +66 -0
- data/lib/generators/seams/admin/templates/lib/configuration.rb.tt +90 -0
- data/lib/generators/seams/admin/templates/lib/context.rb.tt +44 -0
- data/lib/generators/seams/admin/templates/lib/engine.rb.tt +68 -0
- data/lib/generators/seams/admin/templates/spec/factories/admin.rb.tt +10 -0
- data/lib/generators/seams/admin/templates/spec/runtime/admin_boot_spec.rb.tt +604 -0
- data/lib/generators/seams/auth/add_oauth_provider/add_oauth_provider_generator.rb +157 -0
- data/lib/generators/seams/auth/add_oauth_provider/templates/adapter.rb.tt +95 -0
- data/lib/generators/seams/auth/add_oauth_provider/templates/adapter_spec.rb.tt +58 -0
- data/lib/generators/seams/auth/auth_generator.rb +311 -0
- data/lib/generators/seams/auth/templates/README.md.tt +289 -0
- data/lib/generators/seams/auth/templates/app/controllers/oauth/callbacks_controller.rb.tt +80 -0
- data/lib/generators/seams/auth/templates/app/controllers/password_resets_controller.rb.tt +44 -0
- data/lib/generators/seams/auth/templates/app/controllers/registrations_controller.rb.tt +34 -0
- data/lib/generators/seams/auth/templates/app/controllers/sessions_controller.rb.tt +49 -0
- data/lib/generators/seams/auth/templates/app/jobs/application_job.rb.tt +7 -0
- data/lib/generators/seams/auth/templates/app/jobs/cleanup_expired_sessions_job.rb.tt +30 -0
- data/lib/generators/seams/auth/templates/app/mailers/passwords_mailer.rb.tt +15 -0
- data/lib/generators/seams/auth/templates/app/models/api_token.rb.tt +62 -0
- data/lib/generators/seams/auth/templates/app/models/application_record.rb.tt +7 -0
- data/lib/generators/seams/auth/templates/app/models/current.rb.tt +15 -0
- data/lib/generators/seams/auth/templates/app/models/identity.rb.tt +74 -0
- data/lib/generators/seams/auth/templates/app/models/oauth/provider.rb.tt +48 -0
- data/lib/generators/seams/auth/templates/app/models/session.rb.tt +28 -0
- data/lib/generators/seams/auth/templates/app/services/authenticate_identity.rb.tt +31 -0
- data/lib/generators/seams/auth/templates/app/services/generate_api_token.rb.tt +35 -0
- data/lib/generators/seams/auth/templates/app/services/oauth/authenticator.rb.tt +94 -0
- data/lib/generators/seams/auth/templates/app/services/register_identity.rb.tt +57 -0
- data/lib/generators/seams/auth/templates/app/services/reset_password.rb.tt +41 -0
- data/lib/generators/seams/auth/templates/app/services/revoke_api_token.rb.tt +38 -0
- data/lib/generators/seams/auth/templates/app/views/password_resets/edit.html.erb.tt +12 -0
- data/lib/generators/seams/auth/templates/app/views/password_resets/new.html.erb.tt +11 -0
- data/lib/generators/seams/auth/templates/app/views/passwords_mailer/reset_email.html.erb.tt +7 -0
- data/lib/generators/seams/auth/templates/app/views/registrations/new.html.erb.tt +26 -0
- data/lib/generators/seams/auth/templates/app/views/sessions/_oauth_buttons.html.erb.tt +18 -0
- data/lib/generators/seams/auth/templates/app/views/sessions/new.html.erb.tt +17 -0
- data/lib/generators/seams/auth/templates/config/routes.rb.tt +21 -0
- data/lib/generators/seams/auth/templates/db/migrate/create_auth_api_tokens.rb.tt +26 -0
- data/lib/generators/seams/auth/templates/db/migrate/create_auth_identities.rb.tt +29 -0
- data/lib/generators/seams/auth/templates/db/migrate/create_auth_oauth_providers.rb.tt +35 -0
- data/lib/generators/seams/auth/templates/db/migrate/create_auth_sessions.rb.tt +19 -0
- data/lib/generators/seams/auth/templates/lib/auth.rb.tt +39 -0
- data/lib/generators/seams/auth/templates/lib/concerns/api_authenticatable.rb.tt +58 -0
- data/lib/generators/seams/auth/templates/lib/concerns/authenticatable.rb.tt +32 -0
- data/lib/generators/seams/auth/templates/lib/concerns/authentication.rb.tt +60 -0
- data/lib/generators/seams/auth/templates/lib/configuration.rb.tt +45 -0
- data/lib/generators/seams/auth/templates/lib/engine.rb.tt +46 -0
- data/lib/generators/seams/auth/templates/lib/oauth/abstract.rb.tt +87 -0
- data/lib/generators/seams/auth/templates/lib/oauth/github.rb.tt +112 -0
- data/lib/generators/seams/auth/templates/lib/oauth/google.rb.tt +78 -0
- data/lib/generators/seams/auth/templates/lib/tasks/auth_pii.rake.tt +68 -0
- data/lib/generators/seams/auth/templates/spec/factories/auth.rb.tt +38 -0
- data/lib/generators/seams/auth/templates/spec/mailers/passwords_mailer_spec.rb.tt +37 -0
- data/lib/generators/seams/auth/templates/spec/models/api_token_spec.rb.tt +84 -0
- data/lib/generators/seams/auth/templates/spec/models/identity_spec.rb.tt +56 -0
- data/lib/generators/seams/auth/templates/spec/models/oauth/provider_spec.rb.tt +64 -0
- data/lib/generators/seams/auth/templates/spec/models/session_spec.rb.tt +34 -0
- data/lib/generators/seams/auth/templates/spec/runtime/boot_spec.rb.tt +30 -0
- data/lib/generators/seams/auth/templates/spec/runtime/event_payload_spec.rb.tt +29 -0
- data/lib/generators/seams/auth/templates/spec/runtime/login_flow_spec.rb.tt +45 -0
- data/lib/generators/seams/billing/billing_generator.rb +476 -0
- data/lib/generators/seams/billing/templates/README.md.tt +355 -0
- data/lib/generators/seams/billing/templates/app/controllers/admin/lifetime_passes_controller.rb.tt +84 -0
- data/lib/generators/seams/billing/templates/app/controllers/checkout_controller.rb.tt +92 -0
- data/lib/generators/seams/billing/templates/app/controllers/invoices_controller.rb.tt +63 -0
- data/lib/generators/seams/billing/templates/app/controllers/plans_controller.rb.tt +14 -0
- data/lib/generators/seams/billing/templates/app/controllers/portal_controller.rb.tt +45 -0
- data/lib/generators/seams/billing/templates/app/controllers/subscriptions_controller.rb.tt +119 -0
- data/lib/generators/seams/billing/templates/app/controllers/webhooks_controller.rb.tt +98 -0
- data/lib/generators/seams/billing/templates/app/helpers/currency_helper.rb.tt +44 -0
- data/lib/generators/seams/billing/templates/app/jobs/application_job.rb.tt +6 -0
- data/lib/generators/seams/billing/templates/app/jobs/cancel_subscription_job.rb.tt +39 -0
- data/lib/generators/seams/billing/templates/app/jobs/start_subscription_job.rb.tt +32 -0
- data/lib/generators/seams/billing/templates/app/jobs/webhooks/process_event_job.rb.tt +37 -0
- data/lib/generators/seams/billing/templates/app/models/application_record.rb.tt +7 -0
- data/lib/generators/seams/billing/templates/app/models/invoice.rb.tt +35 -0
- data/lib/generators/seams/billing/templates/app/models/lifetime_pass.rb.tt +60 -0
- data/lib/generators/seams/billing/templates/app/models/plan.rb.tt +95 -0
- data/lib/generators/seams/billing/templates/app/models/subscription.rb.tt +31 -0
- data/lib/generators/seams/billing/templates/app/models/webhook_event.rb.tt +13 -0
- data/lib/generators/seams/billing/templates/app/services/checkout_session_service.rb.tt +25 -0
- data/lib/generators/seams/billing/templates/app/services/customers/find_or_create_service.rb.tt +73 -0
- data/lib/generators/seams/billing/templates/app/services/invoices/sync_service.rb.tt +50 -0
- data/lib/generators/seams/billing/templates/app/services/lifetime/create_lifetime_session_service.rb.tt +82 -0
- data/lib/generators/seams/billing/templates/app/services/lifetime/create_pass_from_checkout_service.rb.tt +88 -0
- data/lib/generators/seams/billing/templates/app/services/lifetime/grant_pass_service.rb.tt +80 -0
- data/lib/generators/seams/billing/templates/app/services/lifetime/revoke_pass_service.rb.tt +59 -0
- data/lib/generators/seams/billing/templates/app/services/portal_session_service.rb.tt +23 -0
- data/lib/generators/seams/billing/templates/app/services/service_result.rb.tt +38 -0
- data/lib/generators/seams/billing/templates/app/services/stripe_service.rb.tt +67 -0
- data/lib/generators/seams/billing/templates/app/services/subscriptions/cancel_service.rb.tt +42 -0
- data/lib/generators/seams/billing/templates/app/services/subscriptions/change_plan_service.rb.tt +48 -0
- data/lib/generators/seams/billing/templates/app/services/subscriptions/reactivate_service.rb.tt +28 -0
- data/lib/generators/seams/billing/templates/app/services/webhooks/event_router.rb.tt +54 -0
- data/lib/generators/seams/billing/templates/app/services/webhooks/handler.rb.tt +93 -0
- data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/charge_refunded_handler.rb.tt +18 -0
- data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/checkout_session_completed_handler.rb.tt +58 -0
- data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/invoice_created_handler.rb.tt +16 -0
- data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/invoice_finalized_handler.rb.tt +14 -0
- data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/invoice_handler_base.rb.tt +80 -0
- data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/invoice_paid_handler.rb.tt +12 -0
- data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/invoice_payment_failed_handler.rb.tt +12 -0
- data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/invoice_voided_handler.rb.tt +12 -0
- data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/payment_failed_handler.rb.tt +15 -0
- data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/payment_succeeded_handler.rb.tt +19 -0
- data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/subscription_created_handler.rb.tt +11 -0
- data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/subscription_deleted_handler.rb.tt +15 -0
- data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/subscription_handler_base.rb.tt +92 -0
- data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/subscription_trial_will_end_handler.rb.tt +15 -0
- data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/subscription_updated_handler.rb.tt +11 -0
- data/lib/generators/seams/billing/templates/app/views/admin/lifetime_passes/index.html.erb.tt +36 -0
- data/lib/generators/seams/billing/templates/app/views/admin/lifetime_passes/new.html.erb.tt +37 -0
- data/lib/generators/seams/billing/templates/app/views/checkout/success.html.erb.tt +5 -0
- data/lib/generators/seams/billing/templates/app/views/invoices/index.html.erb.tt +22 -0
- data/lib/generators/seams/billing/templates/app/views/invoices/show.html.erb.tt +14 -0
- data/lib/generators/seams/billing/templates/app/views/plans/index.html.erb.tt +51 -0
- data/lib/generators/seams/billing/templates/app/views/subscriptions/index.html.erb.tt +16 -0
- data/lib/generators/seams/billing/templates/app/views/subscriptions/show.html.erb.tt +25 -0
- data/lib/generators/seams/billing/templates/config/routes.rb.tt +39 -0
- data/lib/generators/seams/billing/templates/db/migrate/create_billing_invoices.rb.tt +32 -0
- data/lib/generators/seams/billing/templates/db/migrate/create_billing_lifetime_passes.rb.tt +43 -0
- data/lib/generators/seams/billing/templates/db/migrate/create_billing_plans.rb.tt +31 -0
- data/lib/generators/seams/billing/templates/db/migrate/create_billing_subscriptions.rb.tt +33 -0
- data/lib/generators/seams/billing/templates/db/migrate/create_billing_webhook_events.rb.tt +24 -0
- data/lib/generators/seams/billing/templates/lib/billing.rb.tt +34 -0
- data/lib/generators/seams/billing/templates/lib/concerns/billable.rb.tt +100 -0
- data/lib/generators/seams/billing/templates/lib/configuration.rb.tt +52 -0
- data/lib/generators/seams/billing/templates/lib/engine.rb.tt +72 -0
- data/lib/generators/seams/billing/templates/lib/gateways/abstract.rb.tt +65 -0
- data/lib/generators/seams/billing/templates/lib/gateways/adyen.rb.tt +16 -0
- data/lib/generators/seams/billing/templates/lib/gateways/paddle.rb.tt +22 -0
- data/lib/generators/seams/billing/templates/lib/gateways/stripe.rb.tt +155 -0
- data/lib/generators/seams/billing/templates/lib/stripe/client.rb.tt +101 -0
- data/lib/generators/seams/billing/templates/lib/stripe/webhook_signature.rb.tt +43 -0
- data/lib/generators/seams/billing/templates/lib/tasks/billing_check.rake.tt +34 -0
- data/lib/generators/seams/billing/templates/spec/factories/billing.rb.tt +65 -0
- data/lib/generators/seams/billing/templates/spec/fixtures/stripe/charge_refunded.json.tt +19 -0
- data/lib/generators/seams/billing/templates/spec/fixtures/stripe/checkout_session_completed.json.tt +17 -0
- data/lib/generators/seams/billing/templates/spec/fixtures/stripe/customer_subscription_created.json.tt +25 -0
- data/lib/generators/seams/billing/templates/spec/fixtures/stripe/customer_subscription_deleted.json.tt +17 -0
- data/lib/generators/seams/billing/templates/spec/fixtures/stripe/customer_subscription_trial_will_end.json.tt +17 -0
- data/lib/generators/seams/billing/templates/spec/fixtures/stripe/customer_subscription_updated.json.tt +28 -0
- data/lib/generators/seams/billing/templates/spec/fixtures/stripe/invoice_created.json.tt +18 -0
- data/lib/generators/seams/billing/templates/spec/fixtures/stripe/invoice_finalized.json.tt +18 -0
- data/lib/generators/seams/billing/templates/spec/fixtures/stripe/invoice_paid.json.tt +19 -0
- data/lib/generators/seams/billing/templates/spec/fixtures/stripe/invoice_payment_failed.json.tt +20 -0
- data/lib/generators/seams/billing/templates/spec/fixtures/stripe/invoice_voided.json.tt +18 -0
- data/lib/generators/seams/billing/templates/spec/fixtures/stripe/payment_intent_payment_failed.json.tt +21 -0
- data/lib/generators/seams/billing/templates/spec/fixtures/stripe/payment_intent_succeeded.json.tt +17 -0
- data/lib/generators/seams/billing/templates/spec/gateways/contract_spec.rb.tt +11 -0
- data/lib/generators/seams/billing/templates/spec/gateways/stripe_spec.rb.tt +53 -0
- data/lib/generators/seams/billing/templates/spec/models/plan_spec.rb.tt +81 -0
- data/lib/generators/seams/billing/templates/spec/models/subscription_spec.rb.tt +43 -0
- data/lib/generators/seams/billing/templates/spec/runtime/boot_spec.rb.tt +38 -0
- data/lib/generators/seams/billing/templates/spec/runtime/webhook_handlers_spec.rb.tt +382 -0
- data/lib/generators/seams/billing/templates/spec/support/shared_examples/a_billing_gateway.rb.tt +100 -0
- data/lib/generators/seams/billing/templates/spec/support/stripe_helpers.rb.tt +59 -0
- data/lib/generators/seams/core/core_generator.rb +191 -0
- data/lib/generators/seams/core/templates/README.md.tt +45 -0
- data/lib/generators/seams/core/templates/app/controllers/concerns/has_current_attributes.rb.tt +71 -0
- data/lib/generators/seams/core/templates/app/models/application_record.rb.tt +7 -0
- data/lib/generators/seams/core/templates/app/models/audit_log.rb.tt +19 -0
- data/lib/generators/seams/core/templates/app/models/concerns/auditable.rb.tt +64 -0
- data/lib/generators/seams/core/templates/app/models/concerns/sluggable.rb.tt +53 -0
- data/lib/generators/seams/core/templates/app/models/concerns/soft_deletable.rb.tt +37 -0
- data/lib/generators/seams/core/templates/app/models/concerns/tenant_scoped.rb.tt +39 -0
- data/lib/generators/seams/core/templates/app/models/current.rb.tt +16 -0
- data/lib/generators/seams/core/templates/app/services/event_publisher.rb.tt +23 -0
- data/lib/generators/seams/core/templates/app/validators/email_format_validator.rb.tt +21 -0
- data/lib/generators/seams/core/templates/db/migrate/create_core_audit_logs.rb.tt +29 -0
- data/lib/generators/seams/core/templates/lib/core.rb.tt +8 -0
- data/lib/generators/seams/core/templates/lib/engine.rb.tt +28 -0
- data/lib/generators/seams/core/templates/spec/concerns/auditable_spec.rb.tt +39 -0
- data/lib/generators/seams/core/templates/spec/concerns/sluggable_spec.rb.tt +29 -0
- data/lib/generators/seams/core/templates/spec/models/audit_log_spec.rb.tt +22 -0
- data/lib/generators/seams/core/templates/spec/runtime/boot_spec.rb.tt +25 -0
- data/lib/generators/seams/core/templates/spec/validators/email_format_validator_spec.rb.tt +29 -0
- data/lib/generators/seams/engine/engine_generator.rb +165 -0
- data/lib/generators/seams/engine/templates/Gemfile.tt +19 -0
- data/lib/generators/seams/engine/templates/LICENSE.tt +21 -0
- data/lib/generators/seams/engine/templates/README.md.tt +40 -0
- data/lib/generators/seams/engine/templates/Rakefile.tt +14 -0
- data/lib/generators/seams/engine/templates/app/application_controller.rb.tt +6 -0
- data/lib/generators/seams/engine/templates/app/application_record.rb.tt +16 -0
- data/lib/generators/seams/engine/templates/config/locales/en.yml.tt +14 -0
- data/lib/generators/seams/engine/templates/config/routes.rb.tt +4 -0
- data/lib/generators/seams/engine/templates/gemspec.tt +20 -0
- data/lib/generators/seams/engine/templates/host_initializer.rb.tt +13 -0
- data/lib/generators/seams/engine/templates/lib/engine.rb.tt +27 -0
- data/lib/generators/seams/engine/templates/lib/root.rb.tt +7 -0
- data/lib/generators/seams/engine/templates/lib/version.rb.tt +5 -0
- data/lib/generators/seams/engine/templates/rubocop.yml.tt +55 -0
- data/lib/generators/seams/engine/templates/spec/example_spec.rb.tt +16 -0
- data/lib/generators/seams/engine/templates/spec/spec_helper.rb.tt +23 -0
- data/lib/generators/seams/install/install_generator.rb +211 -0
- data/lib/generators/seams/install/templates/Dockerfile.tt +52 -0
- data/lib/generators/seams/install/templates/Procfile.tt +14 -0
- data/lib/generators/seams/install/templates/bin_seams.tt +107 -0
- data/lib/generators/seams/install/templates/ci.yml.tt +123 -0
- data/lib/generators/seams/install/templates/deploy.yml.tt +63 -0
- data/lib/generators/seams/install/templates/doc/ARCHITECTURE.md.tt +86 -0
- data/lib/generators/seams/install/templates/docker-entrypoint.tt +27 -0
- data/lib/generators/seams/install/templates/rubocop.yml.tt +33 -0
- data/lib/generators/seams/install/templates/ruby-version.tt +1 -0
- data/lib/generators/seams/install/templates/script/collate_coverage.rb.tt +33 -0
- data/lib/generators/seams/install/templates/script/run_affected_tests.sh.tt +64 -0
- data/lib/generators/seams/install/templates/seams.rake.tt +65 -0
- data/lib/generators/seams/install/templates/seams.rb.tt +9 -0
- data/lib/generators/seams/install/templates/seams_engines.rb.tt +15 -0
- data/lib/generators/seams/notifications/notifications_generator.rb +395 -0
- data/lib/generators/seams/notifications/templates/README.md.tt +269 -0
- data/lib/generators/seams/notifications/templates/app/channels/notification_channel.rb.tt +36 -0
- data/lib/generators/seams/notifications/templates/app/controllers/notifications_controller.rb.tt +58 -0
- data/lib/generators/seams/notifications/templates/app/controllers/preferences_controller.rb.tt +54 -0
- data/lib/generators/seams/notifications/templates/app/javascript/controllers/notification_bell_controller.js.tt +34 -0
- data/lib/generators/seams/notifications/templates/app/jobs/application_job.rb.tt +6 -0
- data/lib/generators/seams/notifications/templates/app/jobs/create_notification_job.rb.tt +31 -0
- data/lib/generators/seams/notifications/templates/app/jobs/send_due_notifications_job.rb.tt +22 -0
- data/lib/generators/seams/notifications/templates/app/jobs/send_notification_job.rb.tt +13 -0
- data/lib/generators/seams/notifications/templates/app/mailers/application_mailer.rb.tt +12 -0
- data/lib/generators/seams/notifications/templates/app/mailers/notification_mailer.rb.tt +23 -0
- data/lib/generators/seams/notifications/templates/app/models/application_record.rb.tt +7 -0
- data/lib/generators/seams/notifications/templates/app/models/delivery.rb.tt +13 -0
- data/lib/generators/seams/notifications/templates/app/models/notification.rb.tt +218 -0
- data/lib/generators/seams/notifications/templates/app/models/notification_preference.rb.tt +29 -0
- data/lib/generators/seams/notifications/templates/app/models/strategies/email.rb.tt +38 -0
- data/lib/generators/seams/notifications/templates/app/models/strategies/in_app.rb.tt +26 -0
- data/lib/generators/seams/notifications/templates/app/models/strategies/sms.rb.tt +33 -0
- data/lib/generators/seams/notifications/templates/app/subscribers/auth_subscriber.rb.tt +71 -0
- data/lib/generators/seams/notifications/templates/app/subscribers/billing_subscriber.rb.tt +127 -0
- data/lib/generators/seams/notifications/templates/app/views/layouts/notifications/mailer.html.erb.tt +22 -0
- data/lib/generators/seams/notifications/templates/app/views/layouts/notifications/mailer.text.erb.tt +4 -0
- data/lib/generators/seams/notifications/templates/app/views/notifications/_bell.html.erb.tt +15 -0
- data/lib/generators/seams/notifications/templates/app/views/notifications/index.html.erb.tt +15 -0
- data/lib/generators/seams/notifications/templates/app/views/templates/billing/invoice_failed.html.erb.tt +4 -0
- data/lib/generators/seams/notifications/templates/app/views/templates/billing/invoice_failed.text.erb.tt +4 -0
- data/lib/generators/seams/notifications/templates/app/views/templates/billing/invoice_paid.html.erb.tt +3 -0
- data/lib/generators/seams/notifications/templates/app/views/templates/billing/invoice_paid.text.erb.tt +3 -0
- data/lib/generators/seams/notifications/templates/app/views/templates/billing/lifetime_granted.html.erb.tt +5 -0
- data/lib/generators/seams/notifications/templates/app/views/templates/billing/lifetime_granted.text.erb.tt +5 -0
- data/lib/generators/seams/notifications/templates/app/views/templates/billing/lifetime_purchased.html.erb.tt +5 -0
- data/lib/generators/seams/notifications/templates/app/views/templates/billing/lifetime_purchased.text.erb.tt +5 -0
- data/lib/generators/seams/notifications/templates/app/views/templates/billing/subscription_canceled.html.erb.tt +4 -0
- data/lib/generators/seams/notifications/templates/app/views/templates/billing/subscription_canceled.text.erb.tt +4 -0
- data/lib/generators/seams/notifications/templates/app/views/templates/billing/subscription_started.html.erb.tt +4 -0
- data/lib/generators/seams/notifications/templates/app/views/templates/billing/subscription_started.text.erb.tt +5 -0
- data/lib/generators/seams/notifications/templates/app/views/templates/billing/subscription_updated.html.erb.tt +3 -0
- data/lib/generators/seams/notifications/templates/app/views/templates/billing/subscription_updated.text.erb.tt +3 -0
- data/lib/generators/seams/notifications/templates/app/views/templates/default.html.erb.tt +10 -0
- data/lib/generators/seams/notifications/templates/app/views/templates/default.text.erb.tt +11 -0
- data/lib/generators/seams/notifications/templates/app/views/templates/welcome.html.erb.tt +6 -0
- data/lib/generators/seams/notifications/templates/app/views/templates/welcome.text.erb.tt +6 -0
- data/lib/generators/seams/notifications/templates/config/initializers/notifications.rb.tt +58 -0
- data/lib/generators/seams/notifications/templates/config/routes.rb.tt +17 -0
- data/lib/generators/seams/notifications/templates/db/migrate/create_notification_deliveries.rb.tt +16 -0
- data/lib/generators/seams/notifications/templates/db/migrate/create_notification_preferences.rb.tt +25 -0
- data/lib/generators/seams/notifications/templates/db/migrate/create_notifications.rb.tt +35 -0
- data/lib/generators/seams/notifications/templates/lib/adapters/abstract.rb.tt +20 -0
- data/lib/generators/seams/notifications/templates/lib/adapters/action_mailer.rb.tt +17 -0
- data/lib/generators/seams/notifications/templates/lib/adapters/null_sms.rb.tt +23 -0
- data/lib/generators/seams/notifications/templates/lib/concerns/notifiable.rb.tt +135 -0
- data/lib/generators/seams/notifications/templates/lib/configuration.rb.tt +24 -0
- data/lib/generators/seams/notifications/templates/lib/engine.rb.tt +35 -0
- data/lib/generators/seams/notifications/templates/lib/notifications.rb.tt +75 -0
- data/lib/generators/seams/notifications/templates/lib/type_registry.rb.tt +74 -0
- data/lib/generators/seams/notifications/templates/spec/factories/notifications.rb.tt +53 -0
- data/lib/generators/seams/notifications/templates/spec/models/delivery_spec.rb.tt +28 -0
- data/lib/generators/seams/notifications/templates/spec/models/notification_preference_spec.rb.tt +46 -0
- data/lib/generators/seams/notifications/templates/spec/models/notification_spec.rb.tt +60 -0
- data/lib/generators/seams/notifications/templates/spec/runtime/bell_broadcast_spec.rb.tt +59 -0
- data/lib/generators/seams/notifications/templates/spec/runtime/billing_subscriber_skip_spec.rb.tt +87 -0
- data/lib/generators/seams/notifications/templates/spec/runtime/boot_spec.rb.tt +39 -0
- data/lib/generators/seams/notifications/templates/spec/runtime/schedule_round_trip_spec.rb.tt +55 -0
- data/lib/generators/seams/remove/remove_generator.rb +259 -0
- data/lib/generators/seams/teams/teams_generator.rb +298 -0
- data/lib/generators/seams/teams/templates/README.md.tt +88 -0
- data/lib/generators/seams/teams/templates/app/controllers/invitations_controller.rb.tt +102 -0
- data/lib/generators/seams/teams/templates/app/controllers/memberships_controller.rb.tt +54 -0
- data/lib/generators/seams/teams/templates/app/controllers/teams_controller.rb.tt +68 -0
- data/lib/generators/seams/teams/templates/app/jobs/application_job.rb.tt +6 -0
- data/lib/generators/seams/teams/templates/app/mailers/invitation_mailer.rb.tt +34 -0
- data/lib/generators/seams/teams/templates/app/models/application_record.rb.tt +7 -0
- data/lib/generators/seams/teams/templates/app/models/current.rb.tt +30 -0
- data/lib/generators/seams/teams/templates/app/models/invitation.rb.tt +36 -0
- data/lib/generators/seams/teams/templates/app/models/membership.rb.tt +36 -0
- data/lib/generators/seams/teams/templates/app/models/team.rb.tt +32 -0
- data/lib/generators/seams/teams/templates/app/subscribers/invitation_subscriber.rb.tt +36 -0
- data/lib/generators/seams/teams/templates/app/views/invitation_mailer/invite.text.erb.tt +8 -0
- data/lib/generators/seams/teams/templates/app/views/invitations/index.html.erb.tt +44 -0
- data/lib/generators/seams/teams/templates/app/views/memberships/index.html.erb.tt +32 -0
- data/lib/generators/seams/teams/templates/app/views/teams/edit.html.erb.tt +28 -0
- data/lib/generators/seams/teams/templates/app/views/teams/index.html.erb.tt +15 -0
- data/lib/generators/seams/teams/templates/app/views/teams/new.html.erb.tt +24 -0
- data/lib/generators/seams/teams/templates/app/views/teams/show.html.erb.tt +17 -0
- data/lib/generators/seams/teams/templates/config/routes.rb.tt +19 -0
- data/lib/generators/seams/teams/templates/db/migrate/create_team_invitations.rb.tt +24 -0
- data/lib/generators/seams/teams/templates/db/migrate/create_team_memberships.rb.tt +25 -0
- data/lib/generators/seams/teams/templates/db/migrate/create_teams.rb.tt +18 -0
- data/lib/generators/seams/teams/templates/lib/concerns/account_scoped.rb.tt +79 -0
- data/lib/generators/seams/teams/templates/lib/concerns/authorization.rb.tt +55 -0
- data/lib/generators/seams/teams/templates/lib/configuration.rb.tt +45 -0
- data/lib/generators/seams/teams/templates/lib/engine.rb.tt +51 -0
- data/lib/generators/seams/teams/templates/lib/teams.rb.tt +22 -0
- data/lib/generators/seams/teams/templates/spec/factories/teams.rb.tt +47 -0
- data/lib/generators/seams/teams/templates/spec/models/invitation_spec.rb.tt +25 -0
- data/lib/generators/seams/teams/templates/spec/models/membership_spec.rb.tt +29 -0
- data/lib/generators/seams/teams/templates/spec/models/team_spec.rb.tt +23 -0
- data/lib/generators/seams/teams/templates/spec/runtime/boot_spec.rb.tt +32 -0
- data/lib/seams/cli/list.rb +111 -0
- data/lib/seams/cli/quality.rb +99 -0
- data/lib/seams/cli/resolve.rb +276 -0
- data/lib/seams/cli/test_changed.rb +116 -0
- data/lib/seams/cli.rb +48 -0
- data/lib/seams/configuration.rb +19 -0
- data/lib/seams/cops/known_queue_names.rb +42 -0
- data/lib/seams/cops/migration_comments.rb +68 -0
- data/lib/seams/cops/no_cross_engine_dependency.rb +58 -0
- data/lib/seams/cops/no_cross_engine_model_access.rb +153 -0
- data/lib/seams/cops.rb +18 -0
- data/lib/seams/event_registry.rb +49 -0
- data/lib/seams/events/adapter.rb +24 -0
- data/lib/seams/events/adapters/active_support.rb +31 -0
- data/lib/seams/events/publisher.rb +178 -0
- data/lib/seams/events.rb +39 -0
- data/lib/seams/generators/dummy_app_writer.rb +424 -0
- data/lib/seams/generators/eject_aware.rb +102 -0
- data/lib/seams/generators/follow_up_generator.rb +148 -0
- data/lib/seams/generators/host_injector.rb +124 -0
- data/lib/seams/generators/sibling_rubocop_writer.rb +77 -0
- data/lib/seams/generators/splicer.rb +217 -0
- data/lib/seams/observability/adapter.rb +33 -0
- data/lib/seams/observability/adapters/rails_logger.rb +59 -0
- data/lib/seams/observability.rb +34 -0
- data/lib/seams/runtime.rb +23 -0
- data/lib/seams/version.rb +5 -0
- data/lib/seams.rb +23 -0
- metadata +493 -0
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "seams"
|
|
4
|
+
require "seams/generators/splicer"
|
|
5
|
+
|
|
6
|
+
module Seams
|
|
7
|
+
module CLI
|
|
8
|
+
# Implementation behind `bin/seams resolve` — gap-report 1.2 from
|
|
9
|
+
# the 2026-05 framework feature-gap survey: the documented escape
|
|
10
|
+
# hatch from seams' generators.
|
|
11
|
+
#
|
|
12
|
+
# Three modes:
|
|
13
|
+
#
|
|
14
|
+
# bin/seams resolve --eject <engine>/<file>
|
|
15
|
+
#
|
|
16
|
+
# Marks a single host file as host-owned. The next regeneration
|
|
17
|
+
# of the engine skips this file. The file already lives in the
|
|
18
|
+
# host's working tree (seams generates "the code is in your
|
|
19
|
+
# repo") — eject just prepends an explicit ownership header
|
|
20
|
+
# and tells the engine generator to leave it alone.
|
|
21
|
+
#
|
|
22
|
+
# bin/seams resolve --list-markers <engine>
|
|
23
|
+
#
|
|
24
|
+
# Lists every `# seams:insertion-point ...` marker the engine
|
|
25
|
+
# ships across all of its templated files. Helps the host
|
|
26
|
+
# operator see which extension points are public contract
|
|
27
|
+
# before writing a follow-up generator.
|
|
28
|
+
#
|
|
29
|
+
# bin/seams resolve --list-ejected
|
|
30
|
+
#
|
|
31
|
+
# Surveys engines/ for files marked with the eject header and
|
|
32
|
+
# lists them. Useful for "what's diverged from the gem".
|
|
33
|
+
#
|
|
34
|
+
# Returns true on success / false on failure. The caller (the
|
|
35
|
+
# `bin/seams` shim) translates that into a non-zero exit code.
|
|
36
|
+
#
|
|
37
|
+
# Several methods here legitimately return true/false to signal
|
|
38
|
+
# success/failure but are command verbs (`run_eject`,
|
|
39
|
+
# `engine_present?` is fine, `fail_with` etc). Rubocop's
|
|
40
|
+
# PredicateMethod cop wants every bool-returning method renamed
|
|
41
|
+
# with a trailing `?`, but that's wrong for the run_* dispatchers
|
|
42
|
+
# (they're imperative, not predicates). AbcSize / CyclomaticComplexity
|
|
43
|
+
# likewise trigger on the run_* methods because CLI command
|
|
44
|
+
# branches are inherently branchy. The cops are disabled at file
|
|
45
|
+
# scope and the methods are kept linear and well-commented.
|
|
46
|
+
# rubocop:disable Naming/PredicateMethod, Metrics/AbcSize, Metrics/CyclomaticComplexity
|
|
47
|
+
class Resolve
|
|
48
|
+
DEFAULT_ENGINES_ROOT = "engines"
|
|
49
|
+
|
|
50
|
+
# Header injected at the top of every ejected file. Position
|
|
51
|
+
# matters: future regenerations check the FIRST line of an
|
|
52
|
+
# existing destination file for this exact prefix.
|
|
53
|
+
EJECT_HEADER_PREFIX = "# seams:ejected from"
|
|
54
|
+
EJECT_HEADER_LINES = lambda do |from|
|
|
55
|
+
<<~HEADER
|
|
56
|
+
#{EJECT_HEADER_PREFIX} #{from}
|
|
57
|
+
# Re-running `bin/rails generate seams:#{from.split(".").first}` will NOT overwrite this file.
|
|
58
|
+
# To return to the gem version: delete this file and re-run the generator.
|
|
59
|
+
HEADER
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Files at this list of relative paths under engines/<engine>/
|
|
63
|
+
# are NOT eject-eligible. See doc note in EjectAware module.
|
|
64
|
+
INELIGIBLE_RELATIVE_PATTERNS = [
|
|
65
|
+
%r{\Adb/migrate/}, # one-shot, host runs them
|
|
66
|
+
%r{\Alib/[^/]+/engine\.rb\z}, # framework-managed boot file
|
|
67
|
+
%r{\Alib/[^/]+/version\.rb\z}, # framework-managed version constant
|
|
68
|
+
/\AGemfile\z/, # engine's own Gemfile
|
|
69
|
+
%r{\A[^/]+\.gemspec\z}, # engine's gemspec
|
|
70
|
+
/\ARakefile\z/ # engine's Rakefile (loads engine tasks)
|
|
71
|
+
].freeze
|
|
72
|
+
|
|
73
|
+
def initialize(mode:, argument: nil, engines_root: DEFAULT_ENGINES_ROOT, output: $stdout, error: $stderr)
|
|
74
|
+
@mode = mode
|
|
75
|
+
@argument = argument
|
|
76
|
+
@engines_root = engines_root
|
|
77
|
+
@output = output
|
|
78
|
+
@error = error
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def call
|
|
82
|
+
case @mode
|
|
83
|
+
when :eject then run_eject
|
|
84
|
+
when :list_markers then run_list_markers
|
|
85
|
+
when :list_ejected then run_list_ejected
|
|
86
|
+
else
|
|
87
|
+
fail_with("unknown mode: #{@mode.inspect}")
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
private
|
|
92
|
+
|
|
93
|
+
# ---- Mode 1: --eject <engine>/<file_relative> ----
|
|
94
|
+
|
|
95
|
+
def run_eject
|
|
96
|
+
return false unless argument_present?(usage: "bin/seams resolve --eject <engine>/<file>")
|
|
97
|
+
|
|
98
|
+
engine, relative = split_eject_argument(@argument)
|
|
99
|
+
return false unless engine && relative
|
|
100
|
+
|
|
101
|
+
return false unless engine_present?(engine)
|
|
102
|
+
return false unless eject_eligible?(engine, relative)
|
|
103
|
+
|
|
104
|
+
full_path = File.join(@engines_root, engine, relative)
|
|
105
|
+
return fail_with("file not found: #{full_path}") unless File.exist?(full_path)
|
|
106
|
+
|
|
107
|
+
contents = File.read(full_path)
|
|
108
|
+
if contents.start_with?(EJECT_HEADER_PREFIX)
|
|
109
|
+
@output.puts("already ejected: #{full_path}")
|
|
110
|
+
return true
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
from = "#{engine}.#{relative}"
|
|
114
|
+
File.write(full_path, EJECT_HEADER_LINES.call(from) + contents)
|
|
115
|
+
line_count = File.read(full_path).each_line.count
|
|
116
|
+
@output.puts("ejected: #{full_path} (lines: #{line_count}; from: #{from})")
|
|
117
|
+
true
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def split_eject_argument(argument)
|
|
121
|
+
# Argument shape: "<engine>/<file/path>". The first segment is
|
|
122
|
+
# the engine; everything after the first slash is the relative
|
|
123
|
+
# path within the engine root. We deliberately match on the
|
|
124
|
+
# FIRST slash so paths like "auth/app/mailers/auth/foo.rb"
|
|
125
|
+
# round-trip correctly.
|
|
126
|
+
if argument.include?("/")
|
|
127
|
+
engine, relative = argument.split("/", 2)
|
|
128
|
+
return [engine, relative] if engine && !engine.empty? && relative && !relative.empty?
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
fail_with("expected '<engine>/<file>', got #{argument.inspect}")
|
|
132
|
+
[nil, nil]
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def eject_eligible?(engine, relative)
|
|
136
|
+
return true unless INELIGIBLE_RELATIVE_PATTERNS.any? { |pattern| pattern.match?(relative) }
|
|
137
|
+
|
|
138
|
+
fail_with(
|
|
139
|
+
"refusing to eject #{engine}/#{relative}: this file is framework-managed " \
|
|
140
|
+
"(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."
|
|
142
|
+
)
|
|
143
|
+
false
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# ---- Mode 2: --list-markers <engine> ----
|
|
147
|
+
|
|
148
|
+
def run_list_markers
|
|
149
|
+
return false unless argument_present?(usage: "bin/seams resolve --list-markers <engine>")
|
|
150
|
+
return false unless engine_present?(@argument)
|
|
151
|
+
|
|
152
|
+
engine_root = File.join(@engines_root, @argument)
|
|
153
|
+
markers = collect_markers(engine_root)
|
|
154
|
+
|
|
155
|
+
if markers.empty?
|
|
156
|
+
@output.puts("#{@argument}: no insertion-point markers found in #{engine_root}/")
|
|
157
|
+
@output.puts(" This engine may not have been retrofitted to Wave 10. " \
|
|
158
|
+
"Re-run `bin/rails generate seams:#{@argument}` to pick up the marker set.")
|
|
159
|
+
return true
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
print_marker_table(markers)
|
|
163
|
+
true
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def collect_markers(engine_root)
|
|
167
|
+
rb_files = Dir.glob(File.join(engine_root, "**", "*.rb"))
|
|
168
|
+
rb_files.flat_map do |path|
|
|
169
|
+
Seams::Generators::Splicer.list_markers(file_path: path).map do |info|
|
|
170
|
+
relative = path.sub(%r{\A#{Regexp.escape(engine_root)}/}, "")
|
|
171
|
+
description = description_for(path, info[:line_number])
|
|
172
|
+
info.merge(file: relative, description: description)
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Best-effort one-line description: read the comment immediately
|
|
178
|
+
# PRECEDING the marker (one or two lines back) — the catalogue
|
|
179
|
+
# convention is to document the marker's purpose in a sibling
|
|
180
|
+
# comment line. Falls back to an empty string if no such comment
|
|
181
|
+
# exists; the table prints "(no description)" in that case.
|
|
182
|
+
def description_for(file_path, marker_line_number)
|
|
183
|
+
return "" unless File.exist?(file_path)
|
|
184
|
+
|
|
185
|
+
lines = File.readlines(file_path)
|
|
186
|
+
# marker_line_number is 1-indexed; the description line, if any,
|
|
187
|
+
# sits immediately above. Guard against marker_line_number == 1
|
|
188
|
+
# explicitly — Ruby's negative array index would otherwise wrap
|
|
189
|
+
# to the LAST line of the file, which is nonsensical here.
|
|
190
|
+
return "" if marker_line_number <= 1
|
|
191
|
+
|
|
192
|
+
candidate = lines[marker_line_number - 2]
|
|
193
|
+
return "" unless candidate
|
|
194
|
+
|
|
195
|
+
stripped = candidate.strip
|
|
196
|
+
return "" unless stripped.start_with?("#")
|
|
197
|
+
return "" if stripped.start_with?("# seams:insertion-point")
|
|
198
|
+
|
|
199
|
+
stripped.sub(/\A#\s?/, "").strip
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def print_marker_table(markers)
|
|
203
|
+
marker_width = markers.map { |m| m[:marker].length }.max
|
|
204
|
+
location_width = markers.map { |m| "#{m[:file]}:#{m[:line_number]}".length }.max
|
|
205
|
+
|
|
206
|
+
markers.each do |info|
|
|
207
|
+
location = "#{info[:file]}:#{info[:line_number]}"
|
|
208
|
+
description = info[:description].empty? ? "(no description)" : %("#{info[:description]}")
|
|
209
|
+
@output.puts("#{info[:marker].ljust(marker_width)} #{location.ljust(location_width)} #{description}")
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# ---- Mode 3: --list-ejected ----
|
|
214
|
+
|
|
215
|
+
def run_list_ejected
|
|
216
|
+
unless Dir.exist?(@engines_root)
|
|
217
|
+
@output.puts("no engines directory at #{@engines_root}/")
|
|
218
|
+
return true
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
ejected = collect_ejected_files
|
|
222
|
+
if ejected.empty?
|
|
223
|
+
@output.puts("no ejected files in #{@engines_root}/")
|
|
224
|
+
return true
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
@output.puts("seams: #{ejected.size} ejected file(s)")
|
|
228
|
+
ejected.each { |path, source| @output.puts(" #{path} (from: #{source})") }
|
|
229
|
+
true
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def collect_ejected_files
|
|
233
|
+
# Cheap two-pass scan: read first 200 bytes of every text file
|
|
234
|
+
# under engines/, look for the prefix. We deliberately skip
|
|
235
|
+
# binaries (anything that isn't .rb / .erb / .yml / .yaml / .rake / .css / .js)
|
|
236
|
+
# because the eject header is always a `#`-comment and only
|
|
237
|
+
# text-ish files carry it.
|
|
238
|
+
text_extensions = %w[.rb .erb .yml .yaml .rake .css .js .txt .md].freeze
|
|
239
|
+
Dir.glob(File.join(@engines_root, "**", "*"))
|
|
240
|
+
.select { |path| File.file?(path) && text_extensions.include?(File.extname(path)) }
|
|
241
|
+
.sort
|
|
242
|
+
.filter_map do |path|
|
|
243
|
+
head = File.read(path, 200)
|
|
244
|
+
next nil unless head.start_with?(EJECT_HEADER_PREFIX)
|
|
245
|
+
|
|
246
|
+
source = head.lines.first.to_s.sub(EJECT_HEADER_PREFIX, "").strip
|
|
247
|
+
[path, source]
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# ---- Shared helpers ----
|
|
252
|
+
|
|
253
|
+
def argument_present?(usage:)
|
|
254
|
+
return true if @argument && !@argument.empty?
|
|
255
|
+
|
|
256
|
+
fail_with("missing argument. Usage: #{usage}")
|
|
257
|
+
false
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def engine_present?(engine)
|
|
261
|
+
engine_root = File.join(@engines_root, engine)
|
|
262
|
+
return true if File.directory?(engine_root)
|
|
263
|
+
|
|
264
|
+
fail_with("engine #{engine.inspect} not found at #{engine_root}/. " \
|
|
265
|
+
"Run `bin/rails generate seams:#{engine}` first.")
|
|
266
|
+
false
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def fail_with(message)
|
|
270
|
+
@error.puts("seams resolve: #{message}")
|
|
271
|
+
false
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
# rubocop:enable Naming/PredicateMethod, Metrics/AbcSize, Metrics/CyclomaticComplexity
|
|
275
|
+
end
|
|
276
|
+
end
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "seams"
|
|
4
|
+
|
|
5
|
+
module Seams
|
|
6
|
+
module CLI
|
|
7
|
+
# Runs RSpec for every engine that has changed against the merge
|
|
8
|
+
# base with `main` (or another base via the `base:` keyword).
|
|
9
|
+
# Falls back to "run every engine's specs" when the merge base
|
|
10
|
+
# cannot be resolved (CI on a shallow clone, no git, etc) — better
|
|
11
|
+
# to over-run than to silently skip.
|
|
12
|
+
#
|
|
13
|
+
# bin/rails seams:test:changed # base = main
|
|
14
|
+
# bin/rails seams:test:changed BASE=develop
|
|
15
|
+
#
|
|
16
|
+
# Returns true when every engine spec run passed; false otherwise.
|
|
17
|
+
# The caller (rake task or bin/seams) translates that to an exit
|
|
18
|
+
# code.
|
|
19
|
+
class TestChanged
|
|
20
|
+
DEFAULT_BASE = "main"
|
|
21
|
+
DEFAULT_ENGINES_ROOT = "engines"
|
|
22
|
+
|
|
23
|
+
def initialize(base: DEFAULT_BASE, engines_root: DEFAULT_ENGINES_ROOT, output: $stdout)
|
|
24
|
+
@base = base
|
|
25
|
+
@engines_root = engines_root
|
|
26
|
+
@output = output
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def call
|
|
30
|
+
engines = changed_engines
|
|
31
|
+
if engines.empty?
|
|
32
|
+
@output.puts("seams:test:changed — no engines changed since #{@base}; skipping.")
|
|
33
|
+
return true
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
@output.puts("seams:test:changed — running specs for #{engines.size} engine(s):")
|
|
37
|
+
engines.each { |name| @output.puts(" - #{name}") }
|
|
38
|
+
@output.puts("")
|
|
39
|
+
|
|
40
|
+
failed = engines.reject { |name| run_engine_specs(name) }
|
|
41
|
+
if failed.empty?
|
|
42
|
+
@output.puts("All affected engine specs passed.")
|
|
43
|
+
true
|
|
44
|
+
else
|
|
45
|
+
@output.puts("Failed engines: #{failed.join(", ")}")
|
|
46
|
+
false
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def changed_engines
|
|
53
|
+
return all_engines unless git_available?
|
|
54
|
+
|
|
55
|
+
merge_base = resolve_merge_base
|
|
56
|
+
return all_engines if merge_base.empty?
|
|
57
|
+
|
|
58
|
+
engine_names_from_diff(merge_base)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Array-form shell-out: no interpolation into a shell, so a
|
|
62
|
+
# @base value like `; rm -rf /` passes as one argv element to
|
|
63
|
+
# git rather than getting evaluated. shell_escape is defence in
|
|
64
|
+
# depth on top of that.
|
|
65
|
+
def resolve_merge_base
|
|
66
|
+
capture_command(["git", "merge-base", shell_escape(@base), "HEAD"]).strip
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def engine_names_from_diff(merge_base)
|
|
70
|
+
diff = capture_command(["git", "diff", "--name-only", merge_base, "HEAD", "--",
|
|
71
|
+
"#{@engines_root}/*"])
|
|
72
|
+
diff.lines
|
|
73
|
+
.filter_map { |line| line.split("/")[1] }
|
|
74
|
+
.uniq
|
|
75
|
+
.sort
|
|
76
|
+
.select { |name| File.directory?(File.join(@engines_root, name)) }
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def capture_command(argv)
|
|
80
|
+
IO.popen(argv, err: File::NULL, &:read)
|
|
81
|
+
rescue StandardError
|
|
82
|
+
""
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def all_engines
|
|
86
|
+
return [] unless Dir.exist?(@engines_root)
|
|
87
|
+
|
|
88
|
+
Dir.children(@engines_root)
|
|
89
|
+
.select { |child| File.directory?(File.join(@engines_root, child)) }
|
|
90
|
+
.reject { |child| child.start_with?(".") }
|
|
91
|
+
.sort
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def run_engine_specs(name)
|
|
95
|
+
spec_dir = File.join(@engines_root, name, "spec")
|
|
96
|
+
return true if Dir.glob(File.join(spec_dir, "**", "*_spec.rb")).empty?
|
|
97
|
+
|
|
98
|
+
@output.puts("=== bundle exec rspec #{spec_dir} ===")
|
|
99
|
+
system("bundle", "exec", "rspec", spec_dir)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def git_available?
|
|
103
|
+
system("which git > /dev/null 2>&1")
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# The base branch comes from a keyword arg or env var — both
|
|
107
|
+
# untrusted. Allow only branch-safe characters before
|
|
108
|
+
# interpolating into a shell-out (we never reach the shell
|
|
109
|
+
# because we shell-escape, but defence in depth keeps the
|
|
110
|
+
# intent explicit).
|
|
111
|
+
def shell_escape(value)
|
|
112
|
+
value.to_s.gsub(%r{[^A-Za-z0-9_\-/.]}, "")
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
data/lib/seams/cli.rb
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "seams"
|
|
4
|
+
require "seams/cli/list"
|
|
5
|
+
require "seams/cli/test_changed"
|
|
6
|
+
require "seams/cli/quality"
|
|
7
|
+
require "seams/cli/resolve"
|
|
8
|
+
|
|
9
|
+
module Seams
|
|
10
|
+
# Top-level CLI aggregator. Each public method delegates to a
|
|
11
|
+
# single-purpose CLI class so the rake tasks (and bin/seams) have
|
|
12
|
+
# one entry point. Returns true on success, false on failure —
|
|
13
|
+
# callers translate that into a process exit code.
|
|
14
|
+
#
|
|
15
|
+
# Seams::CLI.list # bin/rails seams:list
|
|
16
|
+
# Seams::CLI.test_changed(base: "main") # seams:test:changed
|
|
17
|
+
# Seams::CLI.quality # seams:quality:all
|
|
18
|
+
# Seams::CLI.resolve(mode: :eject, ...) # bin/seams resolve --eject ...
|
|
19
|
+
module CLI
|
|
20
|
+
module_function
|
|
21
|
+
|
|
22
|
+
def list(engines_root: "engines", output: $stdout)
|
|
23
|
+
Seams::CLI::List.new(engines_root: engines_root, output: output).call
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def test_changed(base: "main", engines_root: "engines", output: $stdout)
|
|
27
|
+
Seams::CLI::TestChanged.new(
|
|
28
|
+
base: base,
|
|
29
|
+
engines_root: engines_root,
|
|
30
|
+
output: output
|
|
31
|
+
).call
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def quality(engines_root: "engines", output: $stdout)
|
|
35
|
+
Seams::CLI::Quality.new(engines_root: engines_root, output: output).call
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def resolve(mode:, argument: nil, engines_root: "engines", output: $stdout, error: $stderr)
|
|
39
|
+
Seams::CLI::Resolve.new(
|
|
40
|
+
mode: mode,
|
|
41
|
+
argument: argument,
|
|
42
|
+
engines_root: engines_root,
|
|
43
|
+
output: output,
|
|
44
|
+
error: error
|
|
45
|
+
).call
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Seams
|
|
4
|
+
# Global Seams configuration. Set via Seams.configure { |c| ... } in
|
|
5
|
+
# config/initializers/seams.rb of the host application.
|
|
6
|
+
class Configuration
|
|
7
|
+
attr_accessor :event_bus_adapter,
|
|
8
|
+
:observability_adapter,
|
|
9
|
+
:event_namespace_separator,
|
|
10
|
+
:host_app_name
|
|
11
|
+
|
|
12
|
+
def initialize
|
|
13
|
+
@event_bus_adapter = "Seams::Events::Adapters::ActiveSupport"
|
|
14
|
+
@observability_adapter = "Seams::Observability::Adapters::RailsLogger"
|
|
15
|
+
@event_namespace_separator = "."
|
|
16
|
+
@host_app_name = nil
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rubocop"
|
|
4
|
+
|
|
5
|
+
module RuboCop
|
|
6
|
+
module Cop
|
|
7
|
+
module Seams
|
|
8
|
+
# Ensures every `queue_as` call uses a queue name that has been
|
|
9
|
+
# registered in the host application's `.rubocop.yml`. Catches typos
|
|
10
|
+
# and prevents jobs from being silently routed to a queue that no
|
|
11
|
+
# worker is listening on.
|
|
12
|
+
class KnownQueueNames < Base
|
|
13
|
+
MSG = "Queue `%<name>s` is not registered. Add it to .rubocop.yml " \
|
|
14
|
+
"under Seams/KnownQueueNames#KnownQueues, or pick one of: %<known>s."
|
|
15
|
+
|
|
16
|
+
# @!method queue_as_literal?(node)
|
|
17
|
+
def_node_matcher :queue_as_literal?, <<~PATTERN
|
|
18
|
+
(send nil? :queue_as ${sym str})
|
|
19
|
+
PATTERN
|
|
20
|
+
|
|
21
|
+
def on_send(node)
|
|
22
|
+
literal = queue_as_literal?(node)
|
|
23
|
+
return unless literal
|
|
24
|
+
|
|
25
|
+
name = literal.value.to_s
|
|
26
|
+
return if known_queues.include?(name)
|
|
27
|
+
|
|
28
|
+
add_offense(
|
|
29
|
+
node,
|
|
30
|
+
message: format(MSG, name: name, known: known_queues.join(", "))
|
|
31
|
+
)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def known_queues
|
|
37
|
+
Array(cop_config["KnownQueues"]).map(&:to_s)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rubocop"
|
|
4
|
+
|
|
5
|
+
module RuboCop
|
|
6
|
+
module Cop
|
|
7
|
+
module Seams
|
|
8
|
+
# Requires every migration to start with a leading comment block
|
|
9
|
+
# so that future readers know what the migration does, why it was
|
|
10
|
+
# needed, and what its data/downtime implications are.
|
|
11
|
+
class MigrationComments < Base
|
|
12
|
+
MSG = "Migration `%<name>s` must be preceded by a comment block explaining " \
|
|
13
|
+
"what changes and why (data implications, downtime risk, rollback notes)."
|
|
14
|
+
|
|
15
|
+
# Comments that look like directives, not documentation. We strip these
|
|
16
|
+
# out before deciding whether the migration carries a real doc block.
|
|
17
|
+
#
|
|
18
|
+
# The Parser associator already drops the encoding/shebang/frozen_string_literal
|
|
19
|
+
# family of magic comments (skip_directives), so this regex only has to
|
|
20
|
+
# cover the directives Parser leaves attached: Sorbet sigils, RuboCop
|
|
21
|
+
# disable/enable, shareable_constant_value, warn_indent.
|
|
22
|
+
MAGIC_COMMENT = /
|
|
23
|
+
\A\s*\#\s*
|
|
24
|
+
(?:
|
|
25
|
+
frozen_string_literal
|
|
26
|
+
| encoding
|
|
27
|
+
| warn_indent
|
|
28
|
+
| shareable_constant_value
|
|
29
|
+
| typed
|
|
30
|
+
| rubocop:(?:disable|enable|todo)
|
|
31
|
+
)
|
|
32
|
+
/x
|
|
33
|
+
|
|
34
|
+
# @!method migration_class?(node)
|
|
35
|
+
def_node_matcher :migration_class?, <<~PATTERN
|
|
36
|
+
(class (const nil? $_) (send (const (const _ :ActiveRecord) :Migration) :[] _) ...)
|
|
37
|
+
PATTERN
|
|
38
|
+
|
|
39
|
+
def on_class(node)
|
|
40
|
+
name = migration_class?(node)
|
|
41
|
+
return unless name
|
|
42
|
+
return if leading_comment?(node)
|
|
43
|
+
|
|
44
|
+
add_offense(node, message: format(MSG, name: name))
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
# True if the migration class is preceded by at least one comment
|
|
50
|
+
# that the parser associates with this specific class node and that
|
|
51
|
+
# isn't a magic comment / directive.
|
|
52
|
+
#
|
|
53
|
+
# We rely on `ast_with_comments` (Parser::Source::Comment.associate_by_identity)
|
|
54
|
+
# rather than line-based scanning so that comments which actually
|
|
55
|
+
# belong to a sibling class or method above the migration are not
|
|
56
|
+
# misread as documentation for the migration.
|
|
57
|
+
def leading_comment?(node)
|
|
58
|
+
documenting_comments_for(node).any?
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def documenting_comments_for(node)
|
|
62
|
+
comments = processed_source.ast_with_comments&.fetch(node, nil) || []
|
|
63
|
+
comments.reject { |comment| MAGIC_COMMENT.match?(comment.text) }
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rubocop"
|
|
4
|
+
|
|
5
|
+
module RuboCop
|
|
6
|
+
module Cop
|
|
7
|
+
module Seams
|
|
8
|
+
# Flags `require`/`require_relative` calls that pull source files
|
|
9
|
+
# out of another engine. Engines should depend on each other only
|
|
10
|
+
# through public events (Seams::Events::Publisher) and
|
|
11
|
+
# explicitly-exposed concerns — not by requiring private files.
|
|
12
|
+
class NoCrossEngineDependency < Base
|
|
13
|
+
MSG = "Engine `%<own>s` must not require `%<path>s` from another engine. " \
|
|
14
|
+
"Communicate via events or via `%<other>s`'s exposed concerns."
|
|
15
|
+
|
|
16
|
+
# @!method require_call?(node)
|
|
17
|
+
def_node_matcher :require_call?, <<~PATTERN
|
|
18
|
+
(send nil? {:require :require_relative} (str $_))
|
|
19
|
+
PATTERN
|
|
20
|
+
|
|
21
|
+
def on_send(node)
|
|
22
|
+
path = require_call?(node)
|
|
23
|
+
return unless path
|
|
24
|
+
|
|
25
|
+
offending_engine = other_engine_for(path)
|
|
26
|
+
return unless offending_engine
|
|
27
|
+
|
|
28
|
+
add_offense(
|
|
29
|
+
node,
|
|
30
|
+
message: format(MSG, own: own_engine, path: path,
|
|
31
|
+
other: capitalize(offending_engine))
|
|
32
|
+
)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def own_engine
|
|
38
|
+
cop_config["OwnEngine"].to_s
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def other_engines
|
|
42
|
+
Array(cop_config["OtherEngines"]).map(&:to_s)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def other_engine_for(path)
|
|
46
|
+
# Match either "billing/foo" or "../../billing/foo" — the engine
|
|
47
|
+
# name is whichever directory segment matches another engine.
|
|
48
|
+
segments = path.split("/")
|
|
49
|
+
other_engines.find { |engine| segments.include?(engine) }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def capitalize(name)
|
|
53
|
+
name.split(/[_-]/).map(&:capitalize).join
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|