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.
Files changed (414) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +335 -0
  3. data/LICENSE +21 -0
  4. data/README.md +104 -0
  5. data/lib/generators/seams/accounts/accounts_generator.rb +272 -0
  6. data/lib/generators/seams/accounts/templates/README.md.tt +219 -0
  7. data/lib/generators/seams/accounts/templates/app/models/account.rb.tt +124 -0
  8. data/lib/generators/seams/accounts/templates/app/models/application_record.rb.tt +7 -0
  9. data/lib/generators/seams/accounts/templates/app/models/current.rb.tt +38 -0
  10. data/lib/generators/seams/accounts/templates/app/models/membership.rb.tt +114 -0
  11. data/lib/generators/seams/accounts/templates/config/routes.rb.tt +8 -0
  12. data/lib/generators/seams/accounts/templates/db/migrate/create_accounts.rb.tt +29 -0
  13. data/lib/generators/seams/accounts/templates/db/migrate/create_accounts_memberships.rb.tt +49 -0
  14. data/lib/generators/seams/accounts/templates/lib/accounts.rb.tt +21 -0
  15. data/lib/generators/seams/accounts/templates/lib/concerns/account_scoped.rb.tt +97 -0
  16. data/lib/generators/seams/accounts/templates/lib/concerns/authorization.rb.tt +105 -0
  17. data/lib/generators/seams/accounts/templates/lib/configuration.rb.tt +26 -0
  18. data/lib/generators/seams/accounts/templates/lib/engine.rb.tt +55 -0
  19. data/lib/generators/seams/accounts/templates/spec/factories/accounts.rb.tt +49 -0
  20. data/lib/generators/seams/accounts/templates/spec/models/accounts/account_spec.rb.tt +64 -0
  21. data/lib/generators/seams/accounts/templates/spec/models/accounts/membership_spec.rb.tt +99 -0
  22. data/lib/generators/seams/accounts/templates/spec/runtime/accounts_boot_spec.rb.tt +181 -0
  23. data/lib/generators/seams/admin/admin_generator.rb +852 -0
  24. data/lib/generators/seams/admin/templates/README.md.tt +266 -0
  25. data/lib/generators/seams/admin/templates/app/controllers/admin/accounts_controller.rb.tt +16 -0
  26. data/lib/generators/seams/admin/templates/app/controllers/admin/accounts_memberships_controller.rb.tt +16 -0
  27. data/lib/generators/seams/admin/templates/app/controllers/admin/application_controller.rb.tt +282 -0
  28. data/lib/generators/seams/admin/templates/app/controllers/admin/identities_controller.rb.tt +26 -0
  29. data/lib/generators/seams/admin/templates/app/controllers/admin/invitations_controller.rb.tt +14 -0
  30. data/lib/generators/seams/admin/templates/app/controllers/admin/invoices_controller.rb.tt +14 -0
  31. data/lib/generators/seams/admin/templates/app/controllers/admin/lifetime_passes_controller.rb.tt +14 -0
  32. data/lib/generators/seams/admin/templates/app/controllers/admin/notification_preferences_controller.rb.tt +15 -0
  33. data/lib/generators/seams/admin/templates/app/controllers/admin/notifications_controller.rb.tt +18 -0
  34. data/lib/generators/seams/admin/templates/app/controllers/admin/plans_controller.rb.tt +14 -0
  35. data/lib/generators/seams/admin/templates/app/controllers/admin/subscriptions_controller.rb.tt +14 -0
  36. data/lib/generators/seams/admin/templates/app/controllers/admin/teams_controller.rb.tt +14 -0
  37. data/lib/generators/seams/admin/templates/app/controllers/admin/teams_memberships_controller.rb.tt +16 -0
  38. data/lib/generators/seams/admin/templates/app/dashboards/admin/account_dashboard.rb.tt +50 -0
  39. data/lib/generators/seams/admin/templates/app/dashboards/admin/accounts_membership_dashboard.rb.tt +58 -0
  40. data/lib/generators/seams/admin/templates/app/dashboards/admin/identity_dashboard.rb.tt +48 -0
  41. data/lib/generators/seams/admin/templates/app/dashboards/admin/invitation_dashboard.rb.tt +51 -0
  42. data/lib/generators/seams/admin/templates/app/dashboards/admin/invoice_dashboard.rb.tt +67 -0
  43. data/lib/generators/seams/admin/templates/app/dashboards/admin/lifetime_pass_dashboard.rb.tt +65 -0
  44. data/lib/generators/seams/admin/templates/app/dashboards/admin/notification_dashboard.rb.tt +58 -0
  45. data/lib/generators/seams/admin/templates/app/dashboards/admin/notification_preference_dashboard.rb.tt +43 -0
  46. data/lib/generators/seams/admin/templates/app/dashboards/admin/plan_dashboard.rb.tt +72 -0
  47. data/lib/generators/seams/admin/templates/app/dashboards/admin/subscription_dashboard.rb.tt +59 -0
  48. data/lib/generators/seams/admin/templates/app/dashboards/admin/team_dashboard.rb.tt +39 -0
  49. data/lib/generators/seams/admin/templates/app/dashboards/admin/teams_membership_dashboard.rb.tt +43 -0
  50. data/lib/generators/seams/admin/templates/app/policies/admin/platform/account_policy.rb.tt +10 -0
  51. data/lib/generators/seams/admin/templates/app/policies/admin/platform/accounts_membership_policy.rb.tt +10 -0
  52. data/lib/generators/seams/admin/templates/app/policies/admin/platform/application_policy.rb.tt +85 -0
  53. data/lib/generators/seams/admin/templates/app/policies/admin/platform/identity_policy.rb.tt +18 -0
  54. data/lib/generators/seams/admin/templates/app/policies/admin/platform/invitation_policy.rb.tt +9 -0
  55. data/lib/generators/seams/admin/templates/app/policies/admin/platform/invoice_policy.rb.tt +9 -0
  56. data/lib/generators/seams/admin/templates/app/policies/admin/platform/lifetime_pass_policy.rb.tt +9 -0
  57. data/lib/generators/seams/admin/templates/app/policies/admin/platform/notification_policy.rb.tt +9 -0
  58. data/lib/generators/seams/admin/templates/app/policies/admin/platform/notification_preference_policy.rb.tt +9 -0
  59. data/lib/generators/seams/admin/templates/app/policies/admin/platform/plan_policy.rb.tt +11 -0
  60. data/lib/generators/seams/admin/templates/app/policies/admin/platform/subscription_policy.rb.tt +9 -0
  61. data/lib/generators/seams/admin/templates/app/policies/admin/platform/team_policy.rb.tt +9 -0
  62. data/lib/generators/seams/admin/templates/app/policies/admin/platform/teams_membership_policy.rb.tt +9 -0
  63. data/lib/generators/seams/admin/templates/app/policies/admin/tenant/account_policy.rb.tt +33 -0
  64. data/lib/generators/seams/admin/templates/app/policies/admin/tenant/accounts_membership_policy.rb.tt +24 -0
  65. data/lib/generators/seams/admin/templates/app/policies/admin/tenant/application_policy.rb.tt +169 -0
  66. data/lib/generators/seams/admin/templates/app/policies/admin/tenant/identity_policy.rb.tt +67 -0
  67. data/lib/generators/seams/admin/templates/app/policies/admin/tenant/invitation_policy.rb.tt +24 -0
  68. data/lib/generators/seams/admin/templates/app/policies/admin/tenant/invoice_policy.rb.tt +21 -0
  69. data/lib/generators/seams/admin/templates/app/policies/admin/tenant/lifetime_pass_policy.rb.tt +21 -0
  70. data/lib/generators/seams/admin/templates/app/policies/admin/tenant/notification_policy.rb.tt +25 -0
  71. data/lib/generators/seams/admin/templates/app/policies/admin/tenant/notification_preference_policy.rb.tt +23 -0
  72. data/lib/generators/seams/admin/templates/app/policies/admin/tenant/plan_policy.rb.tt +47 -0
  73. data/lib/generators/seams/admin/templates/app/policies/admin/tenant/subscription_policy.rb.tt +22 -0
  74. data/lib/generators/seams/admin/templates/app/policies/admin/tenant/team_policy.rb.tt +28 -0
  75. data/lib/generators/seams/admin/templates/app/policies/admin/tenant/teams_membership_policy.rb.tt +24 -0
  76. data/lib/generators/seams/admin/templates/config/routes.rb.tt +38 -0
  77. data/lib/generators/seams/admin/templates/lib/admin.rb.tt +36 -0
  78. data/lib/generators/seams/admin/templates/lib/concerns/authenticator.rb.tt +66 -0
  79. data/lib/generators/seams/admin/templates/lib/configuration.rb.tt +90 -0
  80. data/lib/generators/seams/admin/templates/lib/context.rb.tt +44 -0
  81. data/lib/generators/seams/admin/templates/lib/engine.rb.tt +68 -0
  82. data/lib/generators/seams/admin/templates/spec/factories/admin.rb.tt +10 -0
  83. data/lib/generators/seams/admin/templates/spec/runtime/admin_boot_spec.rb.tt +604 -0
  84. data/lib/generators/seams/auth/add_oauth_provider/add_oauth_provider_generator.rb +157 -0
  85. data/lib/generators/seams/auth/add_oauth_provider/templates/adapter.rb.tt +95 -0
  86. data/lib/generators/seams/auth/add_oauth_provider/templates/adapter_spec.rb.tt +58 -0
  87. data/lib/generators/seams/auth/auth_generator.rb +311 -0
  88. data/lib/generators/seams/auth/templates/README.md.tt +289 -0
  89. data/lib/generators/seams/auth/templates/app/controllers/oauth/callbacks_controller.rb.tt +80 -0
  90. data/lib/generators/seams/auth/templates/app/controllers/password_resets_controller.rb.tt +44 -0
  91. data/lib/generators/seams/auth/templates/app/controllers/registrations_controller.rb.tt +34 -0
  92. data/lib/generators/seams/auth/templates/app/controllers/sessions_controller.rb.tt +49 -0
  93. data/lib/generators/seams/auth/templates/app/jobs/application_job.rb.tt +7 -0
  94. data/lib/generators/seams/auth/templates/app/jobs/cleanup_expired_sessions_job.rb.tt +30 -0
  95. data/lib/generators/seams/auth/templates/app/mailers/passwords_mailer.rb.tt +15 -0
  96. data/lib/generators/seams/auth/templates/app/models/api_token.rb.tt +62 -0
  97. data/lib/generators/seams/auth/templates/app/models/application_record.rb.tt +7 -0
  98. data/lib/generators/seams/auth/templates/app/models/current.rb.tt +15 -0
  99. data/lib/generators/seams/auth/templates/app/models/identity.rb.tt +74 -0
  100. data/lib/generators/seams/auth/templates/app/models/oauth/provider.rb.tt +48 -0
  101. data/lib/generators/seams/auth/templates/app/models/session.rb.tt +28 -0
  102. data/lib/generators/seams/auth/templates/app/services/authenticate_identity.rb.tt +31 -0
  103. data/lib/generators/seams/auth/templates/app/services/generate_api_token.rb.tt +35 -0
  104. data/lib/generators/seams/auth/templates/app/services/oauth/authenticator.rb.tt +94 -0
  105. data/lib/generators/seams/auth/templates/app/services/register_identity.rb.tt +57 -0
  106. data/lib/generators/seams/auth/templates/app/services/reset_password.rb.tt +41 -0
  107. data/lib/generators/seams/auth/templates/app/services/revoke_api_token.rb.tt +38 -0
  108. data/lib/generators/seams/auth/templates/app/views/password_resets/edit.html.erb.tt +12 -0
  109. data/lib/generators/seams/auth/templates/app/views/password_resets/new.html.erb.tt +11 -0
  110. data/lib/generators/seams/auth/templates/app/views/passwords_mailer/reset_email.html.erb.tt +7 -0
  111. data/lib/generators/seams/auth/templates/app/views/registrations/new.html.erb.tt +26 -0
  112. data/lib/generators/seams/auth/templates/app/views/sessions/_oauth_buttons.html.erb.tt +18 -0
  113. data/lib/generators/seams/auth/templates/app/views/sessions/new.html.erb.tt +17 -0
  114. data/lib/generators/seams/auth/templates/config/routes.rb.tt +21 -0
  115. data/lib/generators/seams/auth/templates/db/migrate/create_auth_api_tokens.rb.tt +26 -0
  116. data/lib/generators/seams/auth/templates/db/migrate/create_auth_identities.rb.tt +29 -0
  117. data/lib/generators/seams/auth/templates/db/migrate/create_auth_oauth_providers.rb.tt +35 -0
  118. data/lib/generators/seams/auth/templates/db/migrate/create_auth_sessions.rb.tt +19 -0
  119. data/lib/generators/seams/auth/templates/lib/auth.rb.tt +39 -0
  120. data/lib/generators/seams/auth/templates/lib/concerns/api_authenticatable.rb.tt +58 -0
  121. data/lib/generators/seams/auth/templates/lib/concerns/authenticatable.rb.tt +32 -0
  122. data/lib/generators/seams/auth/templates/lib/concerns/authentication.rb.tt +60 -0
  123. data/lib/generators/seams/auth/templates/lib/configuration.rb.tt +45 -0
  124. data/lib/generators/seams/auth/templates/lib/engine.rb.tt +46 -0
  125. data/lib/generators/seams/auth/templates/lib/oauth/abstract.rb.tt +87 -0
  126. data/lib/generators/seams/auth/templates/lib/oauth/github.rb.tt +112 -0
  127. data/lib/generators/seams/auth/templates/lib/oauth/google.rb.tt +78 -0
  128. data/lib/generators/seams/auth/templates/lib/tasks/auth_pii.rake.tt +68 -0
  129. data/lib/generators/seams/auth/templates/spec/factories/auth.rb.tt +38 -0
  130. data/lib/generators/seams/auth/templates/spec/mailers/passwords_mailer_spec.rb.tt +37 -0
  131. data/lib/generators/seams/auth/templates/spec/models/api_token_spec.rb.tt +84 -0
  132. data/lib/generators/seams/auth/templates/spec/models/identity_spec.rb.tt +56 -0
  133. data/lib/generators/seams/auth/templates/spec/models/oauth/provider_spec.rb.tt +64 -0
  134. data/lib/generators/seams/auth/templates/spec/models/session_spec.rb.tt +34 -0
  135. data/lib/generators/seams/auth/templates/spec/runtime/boot_spec.rb.tt +30 -0
  136. data/lib/generators/seams/auth/templates/spec/runtime/event_payload_spec.rb.tt +29 -0
  137. data/lib/generators/seams/auth/templates/spec/runtime/login_flow_spec.rb.tt +45 -0
  138. data/lib/generators/seams/billing/billing_generator.rb +476 -0
  139. data/lib/generators/seams/billing/templates/README.md.tt +355 -0
  140. data/lib/generators/seams/billing/templates/app/controllers/admin/lifetime_passes_controller.rb.tt +84 -0
  141. data/lib/generators/seams/billing/templates/app/controllers/checkout_controller.rb.tt +92 -0
  142. data/lib/generators/seams/billing/templates/app/controllers/invoices_controller.rb.tt +63 -0
  143. data/lib/generators/seams/billing/templates/app/controllers/plans_controller.rb.tt +14 -0
  144. data/lib/generators/seams/billing/templates/app/controllers/portal_controller.rb.tt +45 -0
  145. data/lib/generators/seams/billing/templates/app/controllers/subscriptions_controller.rb.tt +119 -0
  146. data/lib/generators/seams/billing/templates/app/controllers/webhooks_controller.rb.tt +98 -0
  147. data/lib/generators/seams/billing/templates/app/helpers/currency_helper.rb.tt +44 -0
  148. data/lib/generators/seams/billing/templates/app/jobs/application_job.rb.tt +6 -0
  149. data/lib/generators/seams/billing/templates/app/jobs/cancel_subscription_job.rb.tt +39 -0
  150. data/lib/generators/seams/billing/templates/app/jobs/start_subscription_job.rb.tt +32 -0
  151. data/lib/generators/seams/billing/templates/app/jobs/webhooks/process_event_job.rb.tt +37 -0
  152. data/lib/generators/seams/billing/templates/app/models/application_record.rb.tt +7 -0
  153. data/lib/generators/seams/billing/templates/app/models/invoice.rb.tt +35 -0
  154. data/lib/generators/seams/billing/templates/app/models/lifetime_pass.rb.tt +60 -0
  155. data/lib/generators/seams/billing/templates/app/models/plan.rb.tt +95 -0
  156. data/lib/generators/seams/billing/templates/app/models/subscription.rb.tt +31 -0
  157. data/lib/generators/seams/billing/templates/app/models/webhook_event.rb.tt +13 -0
  158. data/lib/generators/seams/billing/templates/app/services/checkout_session_service.rb.tt +25 -0
  159. data/lib/generators/seams/billing/templates/app/services/customers/find_or_create_service.rb.tt +73 -0
  160. data/lib/generators/seams/billing/templates/app/services/invoices/sync_service.rb.tt +50 -0
  161. data/lib/generators/seams/billing/templates/app/services/lifetime/create_lifetime_session_service.rb.tt +82 -0
  162. data/lib/generators/seams/billing/templates/app/services/lifetime/create_pass_from_checkout_service.rb.tt +88 -0
  163. data/lib/generators/seams/billing/templates/app/services/lifetime/grant_pass_service.rb.tt +80 -0
  164. data/lib/generators/seams/billing/templates/app/services/lifetime/revoke_pass_service.rb.tt +59 -0
  165. data/lib/generators/seams/billing/templates/app/services/portal_session_service.rb.tt +23 -0
  166. data/lib/generators/seams/billing/templates/app/services/service_result.rb.tt +38 -0
  167. data/lib/generators/seams/billing/templates/app/services/stripe_service.rb.tt +67 -0
  168. data/lib/generators/seams/billing/templates/app/services/subscriptions/cancel_service.rb.tt +42 -0
  169. data/lib/generators/seams/billing/templates/app/services/subscriptions/change_plan_service.rb.tt +48 -0
  170. data/lib/generators/seams/billing/templates/app/services/subscriptions/reactivate_service.rb.tt +28 -0
  171. data/lib/generators/seams/billing/templates/app/services/webhooks/event_router.rb.tt +54 -0
  172. data/lib/generators/seams/billing/templates/app/services/webhooks/handler.rb.tt +93 -0
  173. data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/charge_refunded_handler.rb.tt +18 -0
  174. data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/checkout_session_completed_handler.rb.tt +58 -0
  175. data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/invoice_created_handler.rb.tt +16 -0
  176. data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/invoice_finalized_handler.rb.tt +14 -0
  177. data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/invoice_handler_base.rb.tt +80 -0
  178. data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/invoice_paid_handler.rb.tt +12 -0
  179. data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/invoice_payment_failed_handler.rb.tt +12 -0
  180. data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/invoice_voided_handler.rb.tt +12 -0
  181. data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/payment_failed_handler.rb.tt +15 -0
  182. data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/payment_succeeded_handler.rb.tt +19 -0
  183. data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/subscription_created_handler.rb.tt +11 -0
  184. data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/subscription_deleted_handler.rb.tt +15 -0
  185. data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/subscription_handler_base.rb.tt +92 -0
  186. data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/subscription_trial_will_end_handler.rb.tt +15 -0
  187. data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/subscription_updated_handler.rb.tt +11 -0
  188. data/lib/generators/seams/billing/templates/app/views/admin/lifetime_passes/index.html.erb.tt +36 -0
  189. data/lib/generators/seams/billing/templates/app/views/admin/lifetime_passes/new.html.erb.tt +37 -0
  190. data/lib/generators/seams/billing/templates/app/views/checkout/success.html.erb.tt +5 -0
  191. data/lib/generators/seams/billing/templates/app/views/invoices/index.html.erb.tt +22 -0
  192. data/lib/generators/seams/billing/templates/app/views/invoices/show.html.erb.tt +14 -0
  193. data/lib/generators/seams/billing/templates/app/views/plans/index.html.erb.tt +51 -0
  194. data/lib/generators/seams/billing/templates/app/views/subscriptions/index.html.erb.tt +16 -0
  195. data/lib/generators/seams/billing/templates/app/views/subscriptions/show.html.erb.tt +25 -0
  196. data/lib/generators/seams/billing/templates/config/routes.rb.tt +39 -0
  197. data/lib/generators/seams/billing/templates/db/migrate/create_billing_invoices.rb.tt +32 -0
  198. data/lib/generators/seams/billing/templates/db/migrate/create_billing_lifetime_passes.rb.tt +43 -0
  199. data/lib/generators/seams/billing/templates/db/migrate/create_billing_plans.rb.tt +31 -0
  200. data/lib/generators/seams/billing/templates/db/migrate/create_billing_subscriptions.rb.tt +33 -0
  201. data/lib/generators/seams/billing/templates/db/migrate/create_billing_webhook_events.rb.tt +24 -0
  202. data/lib/generators/seams/billing/templates/lib/billing.rb.tt +34 -0
  203. data/lib/generators/seams/billing/templates/lib/concerns/billable.rb.tt +100 -0
  204. data/lib/generators/seams/billing/templates/lib/configuration.rb.tt +52 -0
  205. data/lib/generators/seams/billing/templates/lib/engine.rb.tt +72 -0
  206. data/lib/generators/seams/billing/templates/lib/gateways/abstract.rb.tt +65 -0
  207. data/lib/generators/seams/billing/templates/lib/gateways/adyen.rb.tt +16 -0
  208. data/lib/generators/seams/billing/templates/lib/gateways/paddle.rb.tt +22 -0
  209. data/lib/generators/seams/billing/templates/lib/gateways/stripe.rb.tt +155 -0
  210. data/lib/generators/seams/billing/templates/lib/stripe/client.rb.tt +101 -0
  211. data/lib/generators/seams/billing/templates/lib/stripe/webhook_signature.rb.tt +43 -0
  212. data/lib/generators/seams/billing/templates/lib/tasks/billing_check.rake.tt +34 -0
  213. data/lib/generators/seams/billing/templates/spec/factories/billing.rb.tt +65 -0
  214. data/lib/generators/seams/billing/templates/spec/fixtures/stripe/charge_refunded.json.tt +19 -0
  215. data/lib/generators/seams/billing/templates/spec/fixtures/stripe/checkout_session_completed.json.tt +17 -0
  216. data/lib/generators/seams/billing/templates/spec/fixtures/stripe/customer_subscription_created.json.tt +25 -0
  217. data/lib/generators/seams/billing/templates/spec/fixtures/stripe/customer_subscription_deleted.json.tt +17 -0
  218. data/lib/generators/seams/billing/templates/spec/fixtures/stripe/customer_subscription_trial_will_end.json.tt +17 -0
  219. data/lib/generators/seams/billing/templates/spec/fixtures/stripe/customer_subscription_updated.json.tt +28 -0
  220. data/lib/generators/seams/billing/templates/spec/fixtures/stripe/invoice_created.json.tt +18 -0
  221. data/lib/generators/seams/billing/templates/spec/fixtures/stripe/invoice_finalized.json.tt +18 -0
  222. data/lib/generators/seams/billing/templates/spec/fixtures/stripe/invoice_paid.json.tt +19 -0
  223. data/lib/generators/seams/billing/templates/spec/fixtures/stripe/invoice_payment_failed.json.tt +20 -0
  224. data/lib/generators/seams/billing/templates/spec/fixtures/stripe/invoice_voided.json.tt +18 -0
  225. data/lib/generators/seams/billing/templates/spec/fixtures/stripe/payment_intent_payment_failed.json.tt +21 -0
  226. data/lib/generators/seams/billing/templates/spec/fixtures/stripe/payment_intent_succeeded.json.tt +17 -0
  227. data/lib/generators/seams/billing/templates/spec/gateways/contract_spec.rb.tt +11 -0
  228. data/lib/generators/seams/billing/templates/spec/gateways/stripe_spec.rb.tt +53 -0
  229. data/lib/generators/seams/billing/templates/spec/models/plan_spec.rb.tt +81 -0
  230. data/lib/generators/seams/billing/templates/spec/models/subscription_spec.rb.tt +43 -0
  231. data/lib/generators/seams/billing/templates/spec/runtime/boot_spec.rb.tt +38 -0
  232. data/lib/generators/seams/billing/templates/spec/runtime/webhook_handlers_spec.rb.tt +382 -0
  233. data/lib/generators/seams/billing/templates/spec/support/shared_examples/a_billing_gateway.rb.tt +100 -0
  234. data/lib/generators/seams/billing/templates/spec/support/stripe_helpers.rb.tt +59 -0
  235. data/lib/generators/seams/core/core_generator.rb +191 -0
  236. data/lib/generators/seams/core/templates/README.md.tt +45 -0
  237. data/lib/generators/seams/core/templates/app/controllers/concerns/has_current_attributes.rb.tt +71 -0
  238. data/lib/generators/seams/core/templates/app/models/application_record.rb.tt +7 -0
  239. data/lib/generators/seams/core/templates/app/models/audit_log.rb.tt +19 -0
  240. data/lib/generators/seams/core/templates/app/models/concerns/auditable.rb.tt +64 -0
  241. data/lib/generators/seams/core/templates/app/models/concerns/sluggable.rb.tt +53 -0
  242. data/lib/generators/seams/core/templates/app/models/concerns/soft_deletable.rb.tt +37 -0
  243. data/lib/generators/seams/core/templates/app/models/concerns/tenant_scoped.rb.tt +39 -0
  244. data/lib/generators/seams/core/templates/app/models/current.rb.tt +16 -0
  245. data/lib/generators/seams/core/templates/app/services/event_publisher.rb.tt +23 -0
  246. data/lib/generators/seams/core/templates/app/validators/email_format_validator.rb.tt +21 -0
  247. data/lib/generators/seams/core/templates/db/migrate/create_core_audit_logs.rb.tt +29 -0
  248. data/lib/generators/seams/core/templates/lib/core.rb.tt +8 -0
  249. data/lib/generators/seams/core/templates/lib/engine.rb.tt +28 -0
  250. data/lib/generators/seams/core/templates/spec/concerns/auditable_spec.rb.tt +39 -0
  251. data/lib/generators/seams/core/templates/spec/concerns/sluggable_spec.rb.tt +29 -0
  252. data/lib/generators/seams/core/templates/spec/models/audit_log_spec.rb.tt +22 -0
  253. data/lib/generators/seams/core/templates/spec/runtime/boot_spec.rb.tt +25 -0
  254. data/lib/generators/seams/core/templates/spec/validators/email_format_validator_spec.rb.tt +29 -0
  255. data/lib/generators/seams/engine/engine_generator.rb +165 -0
  256. data/lib/generators/seams/engine/templates/Gemfile.tt +19 -0
  257. data/lib/generators/seams/engine/templates/LICENSE.tt +21 -0
  258. data/lib/generators/seams/engine/templates/README.md.tt +40 -0
  259. data/lib/generators/seams/engine/templates/Rakefile.tt +14 -0
  260. data/lib/generators/seams/engine/templates/app/application_controller.rb.tt +6 -0
  261. data/lib/generators/seams/engine/templates/app/application_record.rb.tt +16 -0
  262. data/lib/generators/seams/engine/templates/config/locales/en.yml.tt +14 -0
  263. data/lib/generators/seams/engine/templates/config/routes.rb.tt +4 -0
  264. data/lib/generators/seams/engine/templates/gemspec.tt +20 -0
  265. data/lib/generators/seams/engine/templates/host_initializer.rb.tt +13 -0
  266. data/lib/generators/seams/engine/templates/lib/engine.rb.tt +27 -0
  267. data/lib/generators/seams/engine/templates/lib/root.rb.tt +7 -0
  268. data/lib/generators/seams/engine/templates/lib/version.rb.tt +5 -0
  269. data/lib/generators/seams/engine/templates/rubocop.yml.tt +55 -0
  270. data/lib/generators/seams/engine/templates/spec/example_spec.rb.tt +16 -0
  271. data/lib/generators/seams/engine/templates/spec/spec_helper.rb.tt +23 -0
  272. data/lib/generators/seams/install/install_generator.rb +211 -0
  273. data/lib/generators/seams/install/templates/Dockerfile.tt +52 -0
  274. data/lib/generators/seams/install/templates/Procfile.tt +14 -0
  275. data/lib/generators/seams/install/templates/bin_seams.tt +107 -0
  276. data/lib/generators/seams/install/templates/ci.yml.tt +123 -0
  277. data/lib/generators/seams/install/templates/deploy.yml.tt +63 -0
  278. data/lib/generators/seams/install/templates/doc/ARCHITECTURE.md.tt +86 -0
  279. data/lib/generators/seams/install/templates/docker-entrypoint.tt +27 -0
  280. data/lib/generators/seams/install/templates/rubocop.yml.tt +33 -0
  281. data/lib/generators/seams/install/templates/ruby-version.tt +1 -0
  282. data/lib/generators/seams/install/templates/script/collate_coverage.rb.tt +33 -0
  283. data/lib/generators/seams/install/templates/script/run_affected_tests.sh.tt +64 -0
  284. data/lib/generators/seams/install/templates/seams.rake.tt +65 -0
  285. data/lib/generators/seams/install/templates/seams.rb.tt +9 -0
  286. data/lib/generators/seams/install/templates/seams_engines.rb.tt +15 -0
  287. data/lib/generators/seams/notifications/notifications_generator.rb +395 -0
  288. data/lib/generators/seams/notifications/templates/README.md.tt +269 -0
  289. data/lib/generators/seams/notifications/templates/app/channels/notification_channel.rb.tt +36 -0
  290. data/lib/generators/seams/notifications/templates/app/controllers/notifications_controller.rb.tt +58 -0
  291. data/lib/generators/seams/notifications/templates/app/controllers/preferences_controller.rb.tt +54 -0
  292. data/lib/generators/seams/notifications/templates/app/javascript/controllers/notification_bell_controller.js.tt +34 -0
  293. data/lib/generators/seams/notifications/templates/app/jobs/application_job.rb.tt +6 -0
  294. data/lib/generators/seams/notifications/templates/app/jobs/create_notification_job.rb.tt +31 -0
  295. data/lib/generators/seams/notifications/templates/app/jobs/send_due_notifications_job.rb.tt +22 -0
  296. data/lib/generators/seams/notifications/templates/app/jobs/send_notification_job.rb.tt +13 -0
  297. data/lib/generators/seams/notifications/templates/app/mailers/application_mailer.rb.tt +12 -0
  298. data/lib/generators/seams/notifications/templates/app/mailers/notification_mailer.rb.tt +23 -0
  299. data/lib/generators/seams/notifications/templates/app/models/application_record.rb.tt +7 -0
  300. data/lib/generators/seams/notifications/templates/app/models/delivery.rb.tt +13 -0
  301. data/lib/generators/seams/notifications/templates/app/models/notification.rb.tt +218 -0
  302. data/lib/generators/seams/notifications/templates/app/models/notification_preference.rb.tt +29 -0
  303. data/lib/generators/seams/notifications/templates/app/models/strategies/email.rb.tt +38 -0
  304. data/lib/generators/seams/notifications/templates/app/models/strategies/in_app.rb.tt +26 -0
  305. data/lib/generators/seams/notifications/templates/app/models/strategies/sms.rb.tt +33 -0
  306. data/lib/generators/seams/notifications/templates/app/subscribers/auth_subscriber.rb.tt +71 -0
  307. data/lib/generators/seams/notifications/templates/app/subscribers/billing_subscriber.rb.tt +127 -0
  308. data/lib/generators/seams/notifications/templates/app/views/layouts/notifications/mailer.html.erb.tt +22 -0
  309. data/lib/generators/seams/notifications/templates/app/views/layouts/notifications/mailer.text.erb.tt +4 -0
  310. data/lib/generators/seams/notifications/templates/app/views/notifications/_bell.html.erb.tt +15 -0
  311. data/lib/generators/seams/notifications/templates/app/views/notifications/index.html.erb.tt +15 -0
  312. data/lib/generators/seams/notifications/templates/app/views/templates/billing/invoice_failed.html.erb.tt +4 -0
  313. data/lib/generators/seams/notifications/templates/app/views/templates/billing/invoice_failed.text.erb.tt +4 -0
  314. data/lib/generators/seams/notifications/templates/app/views/templates/billing/invoice_paid.html.erb.tt +3 -0
  315. data/lib/generators/seams/notifications/templates/app/views/templates/billing/invoice_paid.text.erb.tt +3 -0
  316. data/lib/generators/seams/notifications/templates/app/views/templates/billing/lifetime_granted.html.erb.tt +5 -0
  317. data/lib/generators/seams/notifications/templates/app/views/templates/billing/lifetime_granted.text.erb.tt +5 -0
  318. data/lib/generators/seams/notifications/templates/app/views/templates/billing/lifetime_purchased.html.erb.tt +5 -0
  319. data/lib/generators/seams/notifications/templates/app/views/templates/billing/lifetime_purchased.text.erb.tt +5 -0
  320. data/lib/generators/seams/notifications/templates/app/views/templates/billing/subscription_canceled.html.erb.tt +4 -0
  321. data/lib/generators/seams/notifications/templates/app/views/templates/billing/subscription_canceled.text.erb.tt +4 -0
  322. data/lib/generators/seams/notifications/templates/app/views/templates/billing/subscription_started.html.erb.tt +4 -0
  323. data/lib/generators/seams/notifications/templates/app/views/templates/billing/subscription_started.text.erb.tt +5 -0
  324. data/lib/generators/seams/notifications/templates/app/views/templates/billing/subscription_updated.html.erb.tt +3 -0
  325. data/lib/generators/seams/notifications/templates/app/views/templates/billing/subscription_updated.text.erb.tt +3 -0
  326. data/lib/generators/seams/notifications/templates/app/views/templates/default.html.erb.tt +10 -0
  327. data/lib/generators/seams/notifications/templates/app/views/templates/default.text.erb.tt +11 -0
  328. data/lib/generators/seams/notifications/templates/app/views/templates/welcome.html.erb.tt +6 -0
  329. data/lib/generators/seams/notifications/templates/app/views/templates/welcome.text.erb.tt +6 -0
  330. data/lib/generators/seams/notifications/templates/config/initializers/notifications.rb.tt +58 -0
  331. data/lib/generators/seams/notifications/templates/config/routes.rb.tt +17 -0
  332. data/lib/generators/seams/notifications/templates/db/migrate/create_notification_deliveries.rb.tt +16 -0
  333. data/lib/generators/seams/notifications/templates/db/migrate/create_notification_preferences.rb.tt +25 -0
  334. data/lib/generators/seams/notifications/templates/db/migrate/create_notifications.rb.tt +35 -0
  335. data/lib/generators/seams/notifications/templates/lib/adapters/abstract.rb.tt +20 -0
  336. data/lib/generators/seams/notifications/templates/lib/adapters/action_mailer.rb.tt +17 -0
  337. data/lib/generators/seams/notifications/templates/lib/adapters/null_sms.rb.tt +23 -0
  338. data/lib/generators/seams/notifications/templates/lib/concerns/notifiable.rb.tt +135 -0
  339. data/lib/generators/seams/notifications/templates/lib/configuration.rb.tt +24 -0
  340. data/lib/generators/seams/notifications/templates/lib/engine.rb.tt +35 -0
  341. data/lib/generators/seams/notifications/templates/lib/notifications.rb.tt +75 -0
  342. data/lib/generators/seams/notifications/templates/lib/type_registry.rb.tt +74 -0
  343. data/lib/generators/seams/notifications/templates/spec/factories/notifications.rb.tt +53 -0
  344. data/lib/generators/seams/notifications/templates/spec/models/delivery_spec.rb.tt +28 -0
  345. data/lib/generators/seams/notifications/templates/spec/models/notification_preference_spec.rb.tt +46 -0
  346. data/lib/generators/seams/notifications/templates/spec/models/notification_spec.rb.tt +60 -0
  347. data/lib/generators/seams/notifications/templates/spec/runtime/bell_broadcast_spec.rb.tt +59 -0
  348. data/lib/generators/seams/notifications/templates/spec/runtime/billing_subscriber_skip_spec.rb.tt +87 -0
  349. data/lib/generators/seams/notifications/templates/spec/runtime/boot_spec.rb.tt +39 -0
  350. data/lib/generators/seams/notifications/templates/spec/runtime/schedule_round_trip_spec.rb.tt +55 -0
  351. data/lib/generators/seams/remove/remove_generator.rb +259 -0
  352. data/lib/generators/seams/teams/teams_generator.rb +298 -0
  353. data/lib/generators/seams/teams/templates/README.md.tt +88 -0
  354. data/lib/generators/seams/teams/templates/app/controllers/invitations_controller.rb.tt +102 -0
  355. data/lib/generators/seams/teams/templates/app/controllers/memberships_controller.rb.tt +54 -0
  356. data/lib/generators/seams/teams/templates/app/controllers/teams_controller.rb.tt +68 -0
  357. data/lib/generators/seams/teams/templates/app/jobs/application_job.rb.tt +6 -0
  358. data/lib/generators/seams/teams/templates/app/mailers/invitation_mailer.rb.tt +34 -0
  359. data/lib/generators/seams/teams/templates/app/models/application_record.rb.tt +7 -0
  360. data/lib/generators/seams/teams/templates/app/models/current.rb.tt +30 -0
  361. data/lib/generators/seams/teams/templates/app/models/invitation.rb.tt +36 -0
  362. data/lib/generators/seams/teams/templates/app/models/membership.rb.tt +36 -0
  363. data/lib/generators/seams/teams/templates/app/models/team.rb.tt +32 -0
  364. data/lib/generators/seams/teams/templates/app/subscribers/invitation_subscriber.rb.tt +36 -0
  365. data/lib/generators/seams/teams/templates/app/views/invitation_mailer/invite.text.erb.tt +8 -0
  366. data/lib/generators/seams/teams/templates/app/views/invitations/index.html.erb.tt +44 -0
  367. data/lib/generators/seams/teams/templates/app/views/memberships/index.html.erb.tt +32 -0
  368. data/lib/generators/seams/teams/templates/app/views/teams/edit.html.erb.tt +28 -0
  369. data/lib/generators/seams/teams/templates/app/views/teams/index.html.erb.tt +15 -0
  370. data/lib/generators/seams/teams/templates/app/views/teams/new.html.erb.tt +24 -0
  371. data/lib/generators/seams/teams/templates/app/views/teams/show.html.erb.tt +17 -0
  372. data/lib/generators/seams/teams/templates/config/routes.rb.tt +19 -0
  373. data/lib/generators/seams/teams/templates/db/migrate/create_team_invitations.rb.tt +24 -0
  374. data/lib/generators/seams/teams/templates/db/migrate/create_team_memberships.rb.tt +25 -0
  375. data/lib/generators/seams/teams/templates/db/migrate/create_teams.rb.tt +18 -0
  376. data/lib/generators/seams/teams/templates/lib/concerns/account_scoped.rb.tt +79 -0
  377. data/lib/generators/seams/teams/templates/lib/concerns/authorization.rb.tt +55 -0
  378. data/lib/generators/seams/teams/templates/lib/configuration.rb.tt +45 -0
  379. data/lib/generators/seams/teams/templates/lib/engine.rb.tt +51 -0
  380. data/lib/generators/seams/teams/templates/lib/teams.rb.tt +22 -0
  381. data/lib/generators/seams/teams/templates/spec/factories/teams.rb.tt +47 -0
  382. data/lib/generators/seams/teams/templates/spec/models/invitation_spec.rb.tt +25 -0
  383. data/lib/generators/seams/teams/templates/spec/models/membership_spec.rb.tt +29 -0
  384. data/lib/generators/seams/teams/templates/spec/models/team_spec.rb.tt +23 -0
  385. data/lib/generators/seams/teams/templates/spec/runtime/boot_spec.rb.tt +32 -0
  386. data/lib/seams/cli/list.rb +111 -0
  387. data/lib/seams/cli/quality.rb +99 -0
  388. data/lib/seams/cli/resolve.rb +276 -0
  389. data/lib/seams/cli/test_changed.rb +116 -0
  390. data/lib/seams/cli.rb +48 -0
  391. data/lib/seams/configuration.rb +19 -0
  392. data/lib/seams/cops/known_queue_names.rb +42 -0
  393. data/lib/seams/cops/migration_comments.rb +68 -0
  394. data/lib/seams/cops/no_cross_engine_dependency.rb +58 -0
  395. data/lib/seams/cops/no_cross_engine_model_access.rb +153 -0
  396. data/lib/seams/cops.rb +18 -0
  397. data/lib/seams/event_registry.rb +49 -0
  398. data/lib/seams/events/adapter.rb +24 -0
  399. data/lib/seams/events/adapters/active_support.rb +31 -0
  400. data/lib/seams/events/publisher.rb +178 -0
  401. data/lib/seams/events.rb +39 -0
  402. data/lib/seams/generators/dummy_app_writer.rb +424 -0
  403. data/lib/seams/generators/eject_aware.rb +102 -0
  404. data/lib/seams/generators/follow_up_generator.rb +148 -0
  405. data/lib/seams/generators/host_injector.rb +124 -0
  406. data/lib/seams/generators/sibling_rubocop_writer.rb +77 -0
  407. data/lib/seams/generators/splicer.rb +217 -0
  408. data/lib/seams/observability/adapter.rb +33 -0
  409. data/lib/seams/observability/adapters/rails_logger.rb +59 -0
  410. data/lib/seams/observability.rb +34 -0
  411. data/lib/seams/runtime.rb +23 -0
  412. data/lib/seams/version.rb +5 -0
  413. data/lib/seams.rb +23 -0
  414. metadata +493 -0
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+
5
+ module Seams
6
+ module Generators
7
+ # Idempotent host-file edits used by every canonical generator.
8
+ # Mix into a Rails::Generators::Base subclass; the methods below
9
+ # delegate to Thor's inject_into_file / append_to_file primitives
10
+ # but skip the edit if the host already contains the snippet.
11
+ #
12
+ # Every method is safe to call when the target file is missing —
13
+ # it prints a yellow `skip` line so the user knows to do the edit
14
+ # themselves later.
15
+ module HostInjector
16
+ def host_inject_gem(name, *args, group: nil)
17
+ gemfile = host_path("Gemfile")
18
+ return host_skip("Gemfile not found — add `gem #{name.inspect}` yourself") unless File.exist?(gemfile)
19
+
20
+ return if File.read(gemfile).match?(/^\s*gem\s+["']#{Regexp.escape(name)}["']/)
21
+
22
+ line = build_gem_line(name, args, group)
23
+ say " inject Gemfile (gem \"#{name}\")", :green
24
+ append_to_file(gemfile, "\n#{line}\n")
25
+ end
26
+
27
+ def host_inject_mount(engine_class:, at:)
28
+ routes = host_path("config/routes.rb")
29
+ unless File.exist?(routes)
30
+ return host_skip("config/routes.rb not found — add `mount #{engine_class}, at: \"#{at}\"` yourself")
31
+ end
32
+
33
+ # Word-boundary match on the engine class name so a sibling
34
+ # `mount Auth::EngineExtras` doesn't trick us into thinking
35
+ # `Auth::Engine` is already mounted. The boundary is "anything
36
+ # that isn't a constant-name character" (`[\w:]` excluded).
37
+ return if File.read(routes).match?(/\bmount\s+#{Regexp.escape(engine_class)}(?![\w:])/)
38
+
39
+ say " inject config/routes.rb (mount #{engine_class})", :green
40
+ inject_into_file(routes, after: routes_draw_anchor) do
41
+ " mount #{engine_class}, at: \"#{at}\"\n"
42
+ end
43
+ end
44
+
45
+ # Matches the common `Rails.application.routes.draw do` forms:
46
+ # plain `do`, `do |routes|` block-arg, `do # comment`, and the
47
+ # rare `Rails::Application.routes.draw`.
48
+ def routes_draw_anchor
49
+ /Rails(?:\.application|::Application)\.routes\.draw\s+do(?:\s*\|[^|]+\|)?[^\n]*\n/
50
+ end
51
+
52
+ def host_inject_include_in_user(concern_name)
53
+ host_inject_include("app/models/user.rb", "User", concern_name, label: "User model")
54
+ end
55
+
56
+ def host_inject_include_in_application_controller(concern_name)
57
+ host_inject_include(
58
+ "app/controllers/application_controller.rb", "ApplicationController",
59
+ concern_name, label: "ApplicationController"
60
+ )
61
+ end
62
+
63
+ # Reverse of host_inject_gem — used by the remove generator.
64
+ # Removes the `gem "<name>"` line if present.
65
+ def host_uninject_gem(name)
66
+ gemfile = host_path("Gemfile")
67
+ return unless File.exist?(gemfile)
68
+
69
+ new_content = File.read(gemfile).gsub(/^\s*gem\s+["']#{Regexp.escape(name)}["'][^\n]*\n/, "")
70
+ File.write(gemfile, new_content)
71
+ say " remove Gemfile (gem \"#{name}\")", :red
72
+ end
73
+
74
+ def host_uninject_mount(engine_class:)
75
+ routes = host_path("config/routes.rb")
76
+ return unless File.exist?(routes)
77
+
78
+ # Word-boundary match — see host_inject_mount. Without it, an
79
+ # unrelated `mount Auth::EngineExtras` would match a remove of
80
+ # `mount Auth::Engine` and silently delete the wrong line.
81
+ pattern = /^\s*mount\s+#{Regexp.escape(engine_class)}(?![\w:])[^\n]*\n/
82
+ new_content = File.read(routes).gsub(pattern, "")
83
+ File.write(routes, new_content)
84
+ say " remove config/routes.rb (mount #{engine_class})", :red
85
+ end
86
+
87
+ def host_uninject_include(file_relative, concern_name)
88
+ full = host_path(file_relative)
89
+ return unless File.exist?(full)
90
+
91
+ new_content = File.read(full).gsub(/^\s*include\s+#{Regexp.escape(concern_name)}\s*\n/, "")
92
+ File.write(full, new_content)
93
+ say " remove #{file_relative} (include #{concern_name})", :red
94
+ end
95
+
96
+ private
97
+
98
+ def host_path(relative)
99
+ File.join(destination_root, relative)
100
+ end
101
+
102
+ def host_skip(message)
103
+ say " skip #{message}", :yellow
104
+ end
105
+
106
+ def host_inject_include(file_relative, class_name, concern_name, label:)
107
+ full = host_path(file_relative)
108
+ return host_skip("#{label} not found — add `include #{concern_name}` yourself") unless File.exist?(full)
109
+
110
+ return if File.read(full).match?(/^\s*include\s+#{Regexp.escape(concern_name)}\s*$/)
111
+
112
+ say " inject #{file_relative} (include #{concern_name})", :green
113
+ inject_into_class(full, class_name, " include #{concern_name}\n")
114
+ end
115
+
116
+ def build_gem_line(name, args, group)
117
+ version_part = args.first.is_a?(String) ? %(, "#{args.first}") : ""
118
+ gem_line = %(gem "#{name}"#{version_part})
119
+ gem_line = "group #{group.inspect} do\n #{gem_line}\nend" if group
120
+ gem_line
121
+ end
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+
5
+ module Seams
6
+ module Generators
7
+ # Rewrites the OtherEngines lists inside every engine's .rubocop.yml
8
+ # so each engine is configured with every OTHER engine as a
9
+ # boundary. Used by both the seams:engine and seams:remove
10
+ # generators — generation adds the new engine, removal prunes the
11
+ # gone one.
12
+ #
13
+ # The replacement is scoped tightly to the OtherEngines key so we
14
+ # never clobber the surrounding ExposedConcerns / Enabled / OwnEngine
15
+ # values. The previous version of this code used a too-greedy
16
+ # lookahead that ate the next sibling key.
17
+ module SiblingRubocopWriter
18
+ module_function
19
+
20
+ MODULE_ACCESS_KEY = "Seams/NoCrossEngineModelAccess"
21
+ DEPENDENCY_KEY = "Seams/NoCrossEngineDependency"
22
+
23
+ # @param engines_root [String] absolute or relative path to engines/
24
+ # @param dirs [Array<String>] directory names of the engines
25
+ # that currently exist on disk
26
+ def rewrite!(engines_root:, dirs:)
27
+ dirs.each do |engine_dir|
28
+ others = (dirs - [engine_dir]).sort
29
+ others_module = others.map { |d| camelcase(d) }
30
+ rubocop_path = File.join(engines_root, engine_dir, ".rubocop.yml")
31
+ next unless File.exist?(rubocop_path)
32
+
33
+ content = File.read(rubocop_path)
34
+ content = replace_other_engines(content, MODULE_ACCESS_KEY, others_module)
35
+ content = replace_other_engines(content, DEPENDENCY_KEY, others)
36
+ File.write(rubocop_path, content)
37
+ end
38
+ end
39
+
40
+ def camelcase(name)
41
+ name.split("_").map(&:capitalize).join
42
+ end
43
+
44
+ # Matches `<cop_key>:` and the very next `OtherEngines:` line under
45
+ # it, replacing only the value of OtherEngines. We don't try to
46
+ # span multiple following keys — the regex stops as soon as it has
47
+ # consumed either the inline `[]` form or the indented list form.
48
+ def replace_other_engines(content, cop_key, values)
49
+ regex = other_engines_regex(cop_key)
50
+ formatted = formatted_block(values)
51
+
52
+ unless regex.match?(content)
53
+ raise ArgumentError,
54
+ "SiblingRubocopWriter could not find a writable `OtherEngines:` " \
55
+ "value under `#{cop_key}`. Hand-edited or flow-style YAML " \
56
+ "(`{ OtherEngines: [Foo] }`) is not supported — restore the " \
57
+ "block-style template and retry."
58
+ end
59
+
60
+ content.sub(regex, "\\1#{formatted}\n")
61
+ end
62
+
63
+ def formatted_block(values)
64
+ return " OtherEngines: []" if values.empty?
65
+
66
+ " OtherEngines:\n#{values.map { |v| " - #{v}" }.join("\n")}"
67
+ end
68
+
69
+ def other_engines_regex(cop_key)
70
+ prefix = "#{Regexp.escape(cop_key)}:[^\\n]*\\n(?:[ \\t]+[^\\n]+\\n)*?"
71
+ empty_form = "[ \\t]*\\[\\][ \\t]*\\n"
72
+ list_form = "[ \\t]*\\n(?: -[ \\t]+[^\\n]+\\n)*"
73
+ Regexp.new("(#{prefix}) OtherEngines:(?:#{empty_form}|#{list_form})")
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,217 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+
5
+ module Seams
6
+ module Generators
7
+ # Idempotent splice operations against files containing
8
+ # `# seams:insertion-point <name>` markers. Wave 10 introduces
9
+ # follow-up generators (e.g. `seams:auth:add_oauth_provider`) that
10
+ # need to extend already-generated engines without re-templating
11
+ # the whole file. The Splicer is the shared primitive: every
12
+ # follow-up generator funnels through these methods.
13
+ #
14
+ # See doc/INSERTION_POINTS.md for the marker format spec and
15
+ # doc/INSERTION_POINTS_CATALOGUE.md for the canonical list of
16
+ # markers each engine ships.
17
+ #
18
+ # Example:
19
+ #
20
+ # Seams::Generators::Splicer.splice_after_marker(
21
+ # file_path: "engines/auth/lib/auth/engine.rb",
22
+ # marker: "auth.engine.events",
23
+ # content: " Seams::EventRegistry.register(\"identity.passkey_added.auth\", emitted_by: \"Auth\")\n"
24
+ # )
25
+ #
26
+ # The Splicer owns four design choices worth surfacing:
27
+ #
28
+ # 1. Markers are looked up by NAME, never by line number. A
29
+ # follow-up generator written today must keep working after
30
+ # the host adds twenty unrelated lines above the marker.
31
+ # 2. Idempotency is checked by string-matching the splice content
32
+ # inside a 50-line window after the marker. Re-running the
33
+ # same splice is a no-op rather than an error.
34
+ # 3. Indentation is auto-detected from the marker line itself.
35
+ # Follow-up generators don't have to know whether the marker
36
+ # sits at column 0, column 4, or column 6.
37
+ # 4. The Splicer is pure file I/O — no Rails dep, no Thor — so
38
+ # it can be tested in isolation and reused outside the
39
+ # generator stack (e.g. by `bin/seams resolve --eject`).
40
+ module Splicer
41
+ module_function
42
+
43
+ # Pattern matched by every Splicer method. The character class
44
+ # for the marker name allows lowercase letters, digits, dot, and
45
+ # underscore — see INSERTION_POINTS.md naming rules.
46
+ MARKER_PREFIX = "# seams:insertion-point"
47
+ MARKER_NAME_RE = /[a-z0-9_.]+/
48
+ MARKER_LINE_RE = /^(\s*)#{Regexp.escape(MARKER_PREFIX)}\s+(#{MARKER_NAME_RE})\s*$/
49
+ IDEMPOTENCY_WINDOW = 50
50
+
51
+ # Result struct returned by every splice operation.
52
+ # `ok?` is the only required predicate; `error` is populated
53
+ # only when ok? is false; `lines_added` is 0 when ok? is true
54
+ # but the splice was a no-op (idempotency hit).
55
+ Result = Struct.new(:ok?, :lines_added, :error, keyword_init: true) do
56
+ def to_s
57
+ ok? ? "Splicer::Result(ok, +#{lines_added})" : "Splicer::Result(error: #{error})"
58
+ end
59
+ end
60
+
61
+ # Splice `content` immediately after the line containing
62
+ # `# seams:insertion-point <marker>`.
63
+ #
64
+ # @param file_path [String] absolute or working-dir-relative path
65
+ # @param marker [String] the marker name, e.g. "auth.engine.events"
66
+ # @param content [String] the snippet to insert. Must end in a newline.
67
+ # @param indent [String, nil] override auto-detected indentation. Pass
68
+ # `""` to insert verbatim, or a string of spaces to
69
+ # prepend to every line of `content`.
70
+ # @return [Result]
71
+ def splice_after_marker(file_path:, marker:, content:, indent: nil)
72
+ splice(file_path: file_path, marker: marker, content: content, indent: indent, position: :after)
73
+ end
74
+
75
+ # Splice `content` immediately before the line containing the
76
+ # marker. Same semantics as +splice_after_marker+ otherwise.
77
+ #
78
+ # @return [Result]
79
+ def splice_before_marker(file_path:, marker:, content:, indent: nil)
80
+ splice(file_path: file_path, marker: marker, content: content, indent: indent, position: :before)
81
+ end
82
+
83
+ # Locate a marker without modifying the file. Useful for follow-up
84
+ # generators that want to verify multiple markers exist before
85
+ # they start writing.
86
+ #
87
+ # @return [Hash, nil] { line_number: 1-indexed, indent: " ", marker: "auth.engine.events" }
88
+ # or nil if the marker isn't present.
89
+ def find_marker(file_path:, marker:)
90
+ return nil unless File.exist?(file_path)
91
+
92
+ lines = File.read(file_path).lines
93
+ lines.each_with_index do |line, index|
94
+ match = line.match(MARKER_LINE_RE)
95
+ next unless match
96
+ next unless match[2] == marker
97
+
98
+ return { line_number: index + 1, indent: match[1], marker: marker }
99
+ end
100
+ nil
101
+ end
102
+
103
+ # Enumerate every `seams:insertion-point` marker in a file in
104
+ # source order. Used by the eject CLI's `--list-markers` flag
105
+ # and by `bin/seams resolve` for human-readable diagnostics.
106
+ #
107
+ # @return [Array<Hash>] each entry shaped like +find_marker+ returns.
108
+ def list_markers(file_path:)
109
+ return [] unless File.exist?(file_path)
110
+
111
+ result = []
112
+ File.read(file_path).lines.each_with_index do |line, index|
113
+ match = line.match(MARKER_LINE_RE)
114
+ next unless match
115
+
116
+ result << { line_number: index + 1, indent: match[1], marker: match[2] }
117
+ end
118
+ result
119
+ end
120
+
121
+ # Internal: shared body for splice_{after,before}_marker.
122
+ def splice(file_path:, marker:, content:, indent:, position:)
123
+ return file_not_found_result(file_path) unless File.exist?(file_path)
124
+
125
+ lines = File.read(file_path).lines
126
+ marker_index = locate_marker_index(lines, marker)
127
+ return marker_not_found_result(marker, file_path) unless marker_index
128
+
129
+ prepared = apply_indent(content, indent || detect_indent(lines[marker_index]))
130
+ write_splice(file_path, lines, prepared, marker_index, position)
131
+ end
132
+
133
+ def write_splice(file_path, lines, prepared, marker_index, position)
134
+ return Result.new(ok?: true, lines_added: 0, error: nil) if already_present?(lines, prepared, marker_index,
135
+ position)
136
+
137
+ File.write(file_path, insert_at(lines, prepared, marker_index, position).join)
138
+ Result.new(ok?: true, lines_added: prepared.lines.size, error: nil)
139
+ end
140
+
141
+ def file_not_found_result(file_path)
142
+ Result.new(ok?: false, lines_added: 0, error: "file not found: #{file_path}")
143
+ end
144
+
145
+ def marker_not_found_result(marker, file_path)
146
+ Result.new(ok?: false, lines_added: 0,
147
+ error: "marker '#{marker}' not found in #{file_path}")
148
+ end
149
+
150
+ # The marker line plus every line in the idempotency window
151
+ # following (or preceding for :before) is inspected. The check
152
+ # looks for the FULL prepared content as a contiguous block,
153
+ # not for individual lines — a partial overlap is treated as
154
+ # "not yet spliced" and re-splices, which is the safer default
155
+ # for a tool that prefers re-runs to silent partials.
156
+ #
157
+ # The window grows to accommodate snippets larger than
158
+ # IDEMPOTENCY_WINDOW lines: a 60-line follow-up splice would
159
+ # never round-trip with a fixed 50-line window because the
160
+ # haystack would be smaller than the needle. The effective
161
+ # window is `max(IDEMPOTENCY_WINDOW, prepared.lines.size)`,
162
+ # which guarantees idempotency regardless of snippet size while
163
+ # keeping the small-splice fast path identical.
164
+ def already_present?(lines, prepared, marker_index, position)
165
+ haystack = haystack_for(lines, marker_index, position, prepared)
166
+ haystack.include?(prepared)
167
+ end
168
+
169
+ def haystack_for(lines, marker_index, position, prepared)
170
+ window = [IDEMPOTENCY_WINDOW, prepared.lines.size].max
171
+ if position == :after
172
+ slice_end = [marker_index + window, lines.size - 1].min
173
+ lines[(marker_index + 1)..slice_end].to_a.join
174
+ else
175
+ slice_start = [marker_index - window, 0].max
176
+ lines[slice_start...marker_index].to_a.join
177
+ end
178
+ end
179
+
180
+ def insert_at(lines, prepared, marker_index, position)
181
+ prepared_lines = prepared.lines
182
+ if position == :after
183
+ lines[0..marker_index] + prepared_lines + (lines[(marker_index + 1)..] || [])
184
+ else
185
+ lines[0...marker_index] + prepared_lines + lines[marker_index..]
186
+ end
187
+ end
188
+
189
+ def locate_marker_index(lines, marker)
190
+ lines.each_with_index do |line, index|
191
+ match = line.match(MARKER_LINE_RE)
192
+ return index if match && match[2] == marker
193
+ end
194
+ nil
195
+ end
196
+
197
+ def detect_indent(marker_line)
198
+ marker_line[/^\s*/].to_s
199
+ end
200
+
201
+ # Apply `indent` to every non-blank line of `content`. Blank
202
+ # lines stay blank — re-indenting them would leave trailing
203
+ # whitespace that some linters (and our own RuboCop config) flag.
204
+ def apply_indent(content, indent)
205
+ return content if indent.empty?
206
+
207
+ content.lines.map do |line|
208
+ if line.strip.empty?
209
+ line
210
+ else
211
+ "#{indent}#{line}"
212
+ end
213
+ end.join
214
+ end
215
+ end
216
+ end
217
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "seams/observability"
4
+
5
+ module Seams
6
+ module Observability
7
+ # Abstract base class for observability adapters. Concrete adapters
8
+ # bridge to the host application's logger / APM (Rails.logger,
9
+ # Datadog, OpenTelemetry, etc.) — engines never speak to those
10
+ # libraries directly.
11
+ class Adapter
12
+ def debug(_message, **_context)
13
+ raise NotImplementedError, "#{self.class} must implement #debug"
14
+ end
15
+
16
+ def info(_message, **_context)
17
+ raise NotImplementedError, "#{self.class} must implement #info"
18
+ end
19
+
20
+ def warn(_message, **_context)
21
+ raise NotImplementedError, "#{self.class} must implement #warn"
22
+ end
23
+
24
+ def error(_message, **_context)
25
+ raise NotImplementedError, "#{self.class} must implement #error"
26
+ end
27
+
28
+ def measure(_operation, **_context, &)
29
+ raise NotImplementedError, "#{self.class} must implement #measure"
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "logger"
4
+ require "seams/observability/adapter"
5
+
6
+ module Seams
7
+ module Observability
8
+ module Adapters
9
+ # Default observability adapter — wraps Rails.logger (or a stdout
10
+ # Logger when Rails isn't booted) and emits messages tagged with
11
+ # the seams namespace plus structured key=value context.
12
+ class RailsLogger < Seams::Observability::Adapter
13
+ def initialize(logger: nil)
14
+ super()
15
+ @logger = logger || default_logger
16
+ end
17
+
18
+ %i[debug info warn error].each do |level|
19
+ define_method(level) do |message, **context|
20
+ @logger.public_send(level, format_message(message, context))
21
+ end
22
+ end
23
+
24
+ def measure(operation, **context)
25
+ start = monotonic_ms
26
+ result = yield
27
+ duration = (monotonic_ms - start).round(2)
28
+ info(operation, **context, duration_ms: duration)
29
+ result
30
+ rescue StandardError => e
31
+ duration = (monotonic_ms - start).round(2)
32
+ error(operation, **context, duration_ms: duration, error: "#{e.class}: #{e.message}")
33
+ raise
34
+ end
35
+
36
+ private
37
+
38
+ def format_message(message, context)
39
+ tags = ["[seams]"]
40
+ tags << "[#{context.delete(:engine)}]" if context[:engine]
41
+ parts = context.map { |k, v| "#{k}=#{v}" }
42
+ ([tags.join(" "), message] + parts).join(" ").strip
43
+ end
44
+
45
+ def monotonic_ms
46
+ Process.clock_gettime(Process::CLOCK_MONOTONIC) * 1_000.0
47
+ end
48
+
49
+ def default_logger
50
+ if defined?(::Rails) && ::Rails.respond_to?(:logger) && ::Rails.logger
51
+ ::Rails.logger
52
+ else
53
+ ::Logger.new($stdout)
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Seams
4
+ # Observability module — structured logging and timing primitives that
5
+ # every engine should use instead of calling Rails.logger directly.
6
+ #
7
+ # Engines call Seams::Observability.adapter.info / .warn / .error /
8
+ # .measure with structured context (engine name, actor id, etc.) so that
9
+ # logs can be parsed and filtered uniformly by the host application's
10
+ # log aggregator.
11
+ module Observability
12
+ class Error < Seams::Error; end
13
+
14
+ class << self
15
+ def adapter
16
+ @adapter ||= build_adapter
17
+ end
18
+
19
+ def reset!
20
+ @adapter = nil
21
+ end
22
+
23
+ private
24
+
25
+ def build_adapter
26
+ klass_name = Seams.configuration.observability_adapter
27
+ Object.const_get(klass_name).new
28
+ rescue NameError => e
29
+ raise Seams::ConfigurationError,
30
+ "Observability adapter #{klass_name.inspect} could not be loaded: #{e.message}"
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Seams
4
+ # Module-level methods for the public Seams API. Lives in its own
5
+ # file so that lib/seams.rb stays a thin require-only wrapper.
6
+ module Runtime
7
+ def configuration
8
+ @configuration ||= Configuration.new
9
+ end
10
+
11
+ def configure
12
+ yield configuration
13
+ end
14
+
15
+ def reset_configuration!
16
+ @configuration = Configuration.new
17
+ Events::Publisher.reset! if defined?(Events::Publisher)
18
+ Observability.reset! if defined?(Observability)
19
+ end
20
+ end
21
+
22
+ extend Runtime
23
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Seams
4
+ VERSION = "0.1.0"
5
+ end
data/lib/seams.rb ADDED
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "seams/version"
4
+
5
+ # Seams — A CLI framework that generates modular Rails engines.
6
+ #
7
+ # See https://github.com/Davidslv/seams for documentation.
8
+ module Seams
9
+ class Error < StandardError; end
10
+ class ConfigurationError < Error; end
11
+ class GeneratorError < Error; end
12
+ end
13
+
14
+ require "seams/configuration"
15
+ require "seams/runtime"
16
+ require "seams/events"
17
+ require "seams/events/adapter"
18
+ require "seams/events/adapters/active_support"
19
+ require "seams/event_registry"
20
+ require "seams/events/publisher"
21
+ require "seams/observability"
22
+ require "seams/observability/adapter"
23
+ require "seams/observability/adapters/rails_logger"