Class: Clickwrap::Generators::InstallGenerator
- Inherits:
-
Rails::Generators::Base
- Object
- Rails::Generators::Base
- Clickwrap::Generators::InstallGenerator
- Includes:
- ActiveRecord::Generators::Migration
- Defined in:
- lib/generators/clickwrap/install_generator.rb
Overview
rails generate clickwrap:install — the adaptive migration, one annotated
initializer, a conventional policy file, placeholder legal content, and an
optional engine mount.
Two things make this installer different from an ordinary one.
First, it asks. Recording an IP address or a provider-estimated city is a decision with consequences for the people using the host application, so every request-evidence field is a separate question in plain English with the consequence stated before the choice — never a category enabled as a side effect of something else.
Second, it refuses to guess. The actor class is inferred only when it is unambiguous, legal text is never invented, and no purpose, retention period or resolver is written on the host's behalf. Proxy provenance is derived from the effective Rails rules themselves rather than invented. An incomplete personal-data choice stops the generator before it writes a file.
The only --request-evidence-recipe is privacy-minimized, a convenient
spelling of the off-by-default posture. It disappears after generation;
it is deliberately not a runtime concept. There is no
record_network_context, record_everything, full_evidence, or
regulation-named mode switch anywhere in this gem, because a flag that
enables a whole category of personal data is the thing this gem exists not
to do, and no flag can make a legal determination on a host's behalf.
Constant Summary collapse
- SUPPORTED_ADAPTERS =
The adapters the gem's portable core behavior is tested against.
%w[sqlite sqlite3 postgresql postgis mysql mysql2 trilogy].freeze
- RECIPES =
%w[privacy-minimized].freeze
- EXISTING_LEGAL_CONVENTIONS =
Places applications already keep the legal text their public routes serve. Both documents must exist for a convention to count; order matters, so a Sitepress-style content directory wins over files a previous install of this generator wrote.
rendered_by_the_applicationseparates the two cases: the Sitepress directory holds pages the application itself renders and serves, which is what makes rendering Clickwrap's snapshot through the application's own Markdown pipeline the right default. The second convention is a previous install's placeholder files, which nothing else renders. [ { dir: "app/content/pages/legal", terms: "terms.html.md", privacy: "privacy.html.md", rendered_by_the_application: true }, { dir: "app/content/legal", terms: "terms.md", privacy: "privacy.md", rendered_by_the_application: false } ].freeze
- IP_GEOLOCATION_FIELDS =
The names the configuration uses, which are also the names this generator's one geolocation flag accepts. There used to be nine separate
class_options here, spelled in the plural, plus a table translating them back into the singular configuration names — twenty-odd lines of generator surface for one list. Clickwrap::Vocabulary::IP_GEOLOCATION_DATA_FIELDS
- OPTIONAL_TABLE_MIGRATIONS =
--- Optional tables ------------------------------------------------------
Seven of the seventeen tables this gem knows about cannot receive a row until a matching configuration is turned on, and every one of those is off by default. Emitting them all anyway makes an installation's schema claim capabilities and data categories that installation does not have.
Each flag adds one migration, and re-running the installer later with the flag adds it then.
clickwrap:hardeningis the precedent: an opt-in generator, not a switch inside a migration nobody re-reads. { with_persisted_presentations: "create_clickwrap_presentation_tables", with_request_evidence: "create_clickwrap_request_evidence_tables", with_integrity: "create_clickwrap_integrity_tables", with_retention_ops: "create_clickwrap_retention_tables", with_external_actions: "create_clickwrap_external_action_tables" }.freeze
- OPTIONAL_TABLE_SUMMARIES =
What each omitted flag would have added, in one line, for the post-install message.
{ with_persisted_presentations: "--with-persisted-presentations presentations retained before submission", with_request_evidence: "--with-request-evidence IP address / user-agent / geolocation annex", with_integrity: "--with-integrity event chaining, anchoring, timestamp attestations", with_retention_ops: "--with-retention-ops legal holds and reviewed disposition plans", with_external_actions: "--with-external-actions the outbox for authorize_external_action!" }.freeze
Class Method Summary collapse
Instance Method Summary collapse
-
#ask_about_request_evidence ⇒ Object
The questions come verbatim from the request-evidence design: each one states the consequence before asking for the choice, because a developer who has not thought about IP addresses this week deserves the context in the question rather than in a document they will read later.
- #create_initializer ⇒ Object
-
#create_legal_content_placeholders ⇒ Object
Placeholders only, and only when the application has no legal text anywhere Clickwrap recognizes.
- #create_migration_file ⇒ Object
- #create_policy_file ⇒ Object
-
#detect_environment! ⇒ Object
Everything this run decides about the host is resolved HERE, before a single file is written, and memoized.
- #display_post_install_message ⇒ Object
- #mount_engine ⇒ Object
- #print_unsupported_adapter_notes ⇒ Object
Class Method Details
.next_migration_number(dir) ⇒ Object
180 181 182 |
# File 'lib/generators/clickwrap/install_generator.rb', line 180 def self.next_migration_number(dir) ActiveRecord::Generators::Base.next_migration_number(dir) end |
Instance Method Details
#ask_about_request_evidence ⇒ Object
The questions come verbatim from the request-evidence design: each one states the consequence before asking for the choice, because a developer who has not thought about IP addresses this week deserves the context in the question rather than in a document they will read later.
218 219 220 221 222 223 224 225 226 |
# File 'lib/generators/clickwrap/install_generator.rb', line 218 def ask_about_request_evidence ask_request_evidence_questions unless skip_request_evidence_questions? validate_request_evidence_choices! # The summary prints in every mode, before any file is written, so the # operator sees every enabled field, purpose, encryption choice, # source posture, access behavior, and retention rule together. summarize_request_evidence_choices end |
#create_initializer ⇒ Object
237 238 239 |
# File 'lib/generators/clickwrap/install_generator.rb', line 237 def create_initializer template "initializer.rb.erb", "config/initializers/clickwrap.rb" end |
#create_legal_content_placeholders ⇒ Object
Placeholders only, and only when the application has no legal text anywhere Clickwrap recognizes. Clickwrap never invents legal text: the words are the host's, reviewed by the host's counsel, and a gem that shipped plausible-looking Terms would be inviting an application to publish text nobody read.
When the app ALREADY serves legal pages (a Sitepress content directory, or a previous install's files), the generated config points at those exact files instead of writing a second set: what people accept must be the same bytes the public legal routes render, and two copies of the Terms is how they silently stop being the same document.
256 257 258 259 260 261 262 263 264 265 |
# File 'lib/generators/clickwrap/install_generator.rb', line 256 def create_legal_content_placeholders if detected_legal_documents say_status :found, "existing legal pages in #{detected_legal_documents[:dir]} — " \ "config/clickwrap.rb points at them; no placeholders written", :green return end write_legal_placeholder "terms.md.erb", "app/content/legal/terms.md" write_legal_placeholder "privacy.md.erb", "app/content/legal/privacy.md" end |
#create_migration_file ⇒ Object
228 229 230 231 232 233 234 235 |
# File 'lib/generators/clickwrap/install_generator.rb', line 228 def create_migration_file migration_template "create_clickwrap_tables.rb.erb", File.join(db_migrate_path, "create_clickwrap_tables.rb") requested_optional_table_migrations.each do |name| migration_template "#{name}.rb.erb", File.join(db_migrate_path, "#{name}.rb") end end |
#create_policy_file ⇒ Object
241 242 243 |
# File 'lib/generators/clickwrap/install_generator.rb', line 241 def create_policy_file template "clickwrap_policies.rb.erb", "config/clickwrap.rb" end |
#detect_environment! ⇒ Object
Everything this run decides about the host is resolved HERE, before a single file is written, and memoized. The steps below create config/initializers/clickwrap.rb, config/clickwrap.rb, and content files; re-reading the environment halfway through would answer a different question than the one this step asked, and the post-install message would describe an application that did not exist when the decisions were made.
190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 |
# File 'lib/generators/clickwrap/install_generator.rb', line 190 def detect_environment! database_adapter primary_key_type devise_detected? rails_authentication_detected? actor_class_name say "\n☑️ Installing clickwrap.", :green say " Database adapter: #{database_adapter || "not detected"}" say " Primary key type: #{primary_key_type_description}" say " Authentication: #{detected_authentication || "none detected (both integrations are optional)"}" say " Actor class: #{actor_class_name || "not inferred — see the initializer"}" return if actor_class_name say "\n⚠️ Clickwrap could not infer your actor class unambiguously.", :yellow say " #{actor_class_reason}", :yellow say " `config.actor_class_name` is therefore left COMMENTED OUT in the", :yellow say " generated initializer, with an explanation beside it. Which record", :yellow say " can act is a security-relevant identity mapping, and a wrong guess", :yellow say " attributes evidence to the wrong kind of record for years. Set it", :yellow say " yourself, or re-run with --actor-class=YourModel.", :yellow end |
#display_post_install_message ⇒ Object
301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 |
# File 'lib/generators/clickwrap/install_generator.rb', line 301 def say "\n☑️ The `clickwrap` gem has been installed.", :green say "\nTo complete the setup:" step = 0 say " #{step += 1}. Run 'rails db:migrate' to create the clickwrap tables." say " ⚠️ You must run migrations before starting your app!", :yellow say_optional_tables say " #{step += 1}. Declare which records can act:" say " class #{actor_class_name || "User"} < ApplicationRecord" say " has_clickwraps" say " end" say " (and set `config.actor_class_name` in the initializer)" unless actor_class_name if detected_legal_documents say " #{step += 1}. Your existing legal pages are the documents (same bytes people" say " read and accept):" say " #{terms_document_path}" say " #{privacy_document_path}" if documents_needing_an_explicit_version.empty? say " Each page names its own version in its front matter, so config/clickwrap.rb" say " declares both without a `version:` line." else say " A page that names its own version in its front matter is declared in" say " config/clickwrap.rb without a `version:` line." say_documents_needing_an_explicit_version end else say " #{step += 1}. Replace the placeholder legal text with your own reviewed documents:" say " #{terms_document_path}" say " #{privacy_document_path}" say " Keep the `last_updated:` front matter at the top of each file accurate:" say " that line is the version label, so a text change is one edit in one file." end say " #{step += 1}. Publish immutable snapshots:" say " bin/rails clickwrap:publish" say " Deploys do this for you — publishing rides `db:prepare`, so a snapshot" say " exists before the server takes traffic." say " #{step += 1}. Render the policy and its bound submit action:" say " <%= form.clickwrap :signup, submit: \"Create account\" %>" say " That renders ONE line — a checkbox and a sentence with your documents" say " linked inside it:" say " [ ] I agree to the Terms and I acknowledge the Privacy Notice." say " Point each document at the page people read it on, and the links in that" say " sentence go there:" say " Clickwrap.document :terms, from: ..., link: \"/legal/terms\"" step = say_the_authentication_door_step(step) say " #{step += 1}. Set up your test suite (presentations refuse unpublished documents" say " in tests exactly as in production):" say " # test/test_helper.rb" say " class ActiveSupport::TestCase" say " include Clickwrap::TestHelpers" say " parallelize_setup { Clickwrap.publish! } # per parallel worker..." say " end" say " Clickwrap.publish! # ...and once per process" unless @mounted_engine say " #{step += 1}. Mount the standalone capture/receipt/withdrawal screens when you want them:" say " # config/routes.rb" say " mount Clickwrap::Engine => \"/agreements\"" end if any_ip_geolocation_field? say " #{step + 1}. Ensure #{ip_geolocation_resolver_class_name} and its data source", :yellow say " are available in every environment. The initializer uses the resolver you", :yellow say " explicitly selected; Clickwrap refuses to boot with fields it cannot resolve.", :yellow end print_review_checklist end |
#mount_engine ⇒ Object
267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 |
# File 'lib/generators/clickwrap/install_generator.rb', line 267 def mount_engine return if [:skip_routes] return say_missing_routes_file unless routes_file? return say_already_mounted if engine_already_mounted? return mount_engine_without_asking unless interactive? return unless ask_question(<<~QUESTION) Mount the Clickwrap engine at "/agreements" in config/routes.rb? It adds actor-owned capture, receipt, consent-withdrawal, and document-history screens using your parent controller, layout, locale, and authorization callbacks — so a required agreement can be completed in place instead of becoming a dead end. Nothing is exposed publicly: access still goes through your own authorization callbacks. [y/N] QUESTION route "mount Clickwrap::Engine => \"/agreements\"" @mounted_engine = true end |
#print_unsupported_adapter_notes ⇒ Object
286 287 288 289 290 291 292 293 294 295 296 297 298 299 |
# File 'lib/generators/clickwrap/install_generator.rb', line 286 def print_unsupported_adapter_notes return if supported_adapter? say "\n⚠️ #{database_adapter || "Your database adapter"} is outside the set clickwrap tests.", :yellow say " Tested: SQLite, PostgreSQL, and MySQL. On anything else these are", :yellow say " unverified rather than known-broken, and worth checking yourself:", :yellow say " • the JSON/JSONB column types and their defaults in the migration;", :yellow say " • long document bodies (a silently truncated agreement is the", :yellow say " worst failure this gem has);", :yellow say " • whether the unique index on (policy_key, idempotency_key) really", :yellow say " rejects a duplicate submit under concurrency; and", :yellow say " • `rails generate clickwrap:hardening --database`, whose update and", :yellow say " delete protections are written for PostgreSQL only.", :yellow end |