tokenize_attr 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: abf1c8d9447a7b940dca5bd4759bf60c82ffb967dfcffa18d5df14b41512cfe2
4
- data.tar.gz: c88d3236ac9fae09d5c9d879f4ac0baba43df11ebe8408db5d039fae58b2bd6c
3
+ metadata.gz: 75324e12ad6bd3f86dbd34d10e2a9784d1d7235c0bd821006cd9129008b9835d
4
+ data.tar.gz: 489806ddb67867c16c7e1a09af4da63784044da15fe30b6c1579ce37f326e1ad
5
5
  SHA512:
6
- metadata.gz: f50afa2586d8689212b18a1577da21b372cf499fb1022279475ed609adc0dea4ddcfa712d7ecaa6d70a9fba38732886b68445737f82f8541cd0cf8ba620da4b5
7
- data.tar.gz: d6e087c763c99973351e884a3cb81b7a13527b04e8664cbe15946fbb1bb77d7fe5bec4a49dfc1827271206f827681c68c3ca782f4386a3a0b76e023edee827b0
6
+ metadata.gz: 4a2dbbc09633a58b9c62f813858318b99dd5f1b33a74033b7d11b6531be139e1155e8f837e92fca1894c7468063877109fa6c1a6add0ab9481798fac88f27273
7
+ data.tar.gz: ab5dc72978cde93bb5093357a52881d9f66be064247d7dc7cfbcbfa3404faedbd565eb314885e29f66f0fa7c57fa796ba8da0d87708bbbf03a5f9e8b83c50317
data/AGENTS.md CHANGED
@@ -27,22 +27,32 @@ unavailable), `tokenize` installs a `before_create` callback that:
27
27
  5. Retries up to `retries` times (default 3).
28
28
  6. Raises `TokenizeAttr::RetryExceededError` if all retries are exhausted.
29
29
 
30
+ **Rails integration** is wired via an installer task. Running
31
+ `rails tokenize_attr:install` generates
32
+ `config/initializers/tokenize_attr.rb` which calls
33
+ `ActiveSupport.on_load(:active_record)` to include `TokenizeAttr::Concern`
34
+ into every `ActiveRecord::Base` subclass automatically.
35
+
30
36
  ---
31
37
 
32
38
  ## File map
33
39
 
34
- | File | Role |
35
- |-----------------------------------|-------------------------------------------------------------------|
36
- | `lib/tokenize_attr.rb` | Entry point. Requires sub-files and registers the on_load hook. |
37
- | `lib/tokenize_attr/version.rb` | `TokenizeAttr::VERSION` constant. |
38
- | `lib/tokenize_attr/errors.rb` | `TokenizeAttr::Error` and `TokenizeAttr::RetryExceededError`. |
39
- | `lib/tokenize_attr/tokenizer.rb` | `TokenizeAttr::Tokenizer` — all token-generation logic (private). |
40
- | `lib/tokenize_attr/concern.rb` | `TokenizeAttr::Concern` — thin concern with the `tokenize` macro. |
41
- | `test/test_helper.rb` | In-memory SQLite setup, loads the gem. |
42
- | `test/test_tokenize_attr.rb` | Full test suite. |
43
- | `sig/tokenize_attr.rbs` | RBS type signatures. |
44
- | `llms/overview.md` | Internal design notes for LLMs. |
45
- | `llms/usage.md` | Common usage patterns for LLMs. |
40
+ | File | Role |
41
+ |----------------------------------------|-------------------------------------------------------------------|
42
+ | `lib/tokenize_attr.rb` | Entry point. Requires sub-files; loads Railtie when in Rails. |
43
+ | `lib/tokenize_attr/version.rb` | `TokenizeAttr::VERSION` constant. |
44
+ | `lib/tokenize_attr/errors.rb` | `TokenizeAttr::Error` and `TokenizeAttr::RetryExceededError`. |
45
+ | `lib/tokenize_attr/tokenizer.rb` | `TokenizeAttr::Tokenizer` — all token-generation logic (private). |
46
+ | `lib/tokenize_attr/concern.rb` | `TokenizeAttr::Concern` — thin concern with the `tokenize` macro. |
47
+ | `lib/tokenize_attr/installer.rb` | `TokenizeAttr::Installer` writes/removes the Rails initializer. |
48
+ | `lib/tokenize_attr/railtie.rb` | `TokenizeAttr::Railtie` registers rake tasks in Rails. |
49
+ | `lib/tasks/tokenize_attr.rake` | `tokenize_attr:install` and `tokenize_attr:uninstall` rake tasks. |
50
+ | `test/test_helper.rb` | In-memory SQLite setup; simulates the installed initializer. |
51
+ | `test/test_tokenize_attr.rb` | Full integration test suite. |
52
+ | `test/tokenize_attr/installer_test.rb` | Unit tests for `TokenizeAttr::Installer`. |
53
+ | `sig/tokenize_attr.rbs` | RBS type signatures. |
54
+ | `llms/overview.md` | Internal design notes for LLMs. |
55
+ | `llms/usage.md` | Common usage patterns for LLMs. |
46
56
 
47
57
  ---
48
58
 
@@ -80,6 +90,14 @@ mixed into the user's model class. This eliminates any risk of method-name
80
90
  collisions for models that happen to define methods with similar names.
81
91
  `Concern` only exposes the single public `tokenize` macro.
82
92
 
93
+ ### Why use an installer task instead of an inline `on_load`?
94
+ The installer pattern (also used by `inquiry_attrs`) gives host applications
95
+ explicit, auditable control over when the concern is included. The generated
96
+ initializer is checked into the host app's source control, making the
97
+ integration visible and easy to remove or customise. An inline `on_load` in
98
+ the gem's main file would silently include the concern on every `require`,
99
+ which is less predictable in non-Rails contexts.
100
+
83
101
  ---
84
102
 
85
103
  ## Guardrails
@@ -94,6 +112,8 @@ collisions for models that happen to define methods with similar names.
94
112
  callers can handle it.
95
113
  - **Keep `TokenizeAttr::Tokenizer` methods private** (except `apply`). The
96
114
  class is `@api private`; only `apply` is the stable internal contract.
115
+ - **`TokenizeAttr::Installer` must not depend on Rake or Rails**. All
116
+ file-system logic lives in plain Ruby so it can be unit-tested in isolation.
97
117
 
98
118
  ---
99
119
 
@@ -108,3 +128,8 @@ collisions for models that happen to define methods with similar names.
108
128
  - Model classes share `self.table_name = "records"` so the schema stays flat.
109
129
  - All column names used by test models must exist in the `records` table
110
130
  defined in `test/test_helper.rb`.
131
+ - `test/test_helper.rb` simulates the initializer generated by
132
+ `rails tokenize_attr:install` by calling `ActiveSupport.on_load(:active_record)`
133
+ manually — no full Rails boot is required.
134
+ - Installer tests in `test/tokenize_attr/installer_test.rb` use `Dir.mktmpdir`
135
+ to create a throwaway filesystem tree; no Rails environment is needed.
data/CHANGELOG.md ADDED
@@ -0,0 +1,51 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.2.0] - 2026-03-07
11
+
12
+ ### Added
13
+ - `TokenizeAttr::Installer` class — manages creation and removal of the
14
+ Rails initializer (`config/initializers/tokenize_attr.rb`) that wires the
15
+ gem into `ActiveRecord::Base` via `ActiveSupport.on_load(:active_record)`.
16
+ - `TokenizeAttr::Railtie` — registers `rails tokenize_attr:install` and
17
+ `rails tokenize_attr:uninstall` rake tasks in Rails applications.
18
+ - `lib/tasks/tokenize_attr.rake` — the rake task implementations.
19
+ - `test/tokenize_attr/installer_test.rb` — full unit-test coverage for the
20
+ installer (create, skip-if-exists, uninstall, content validity).
21
+
22
+ ### Changed
23
+ - `lib/tokenize_attr.rb` no longer calls `ActiveSupport.on_load` directly.
24
+ The `on_load` hook is now installed by the generated initializer
25
+ (`rails tokenize_attr:install`) rather than at require-time. This mirrors
26
+ the pattern used by `inquiry_attrs` and gives host applications explicit
27
+ control over when and whether the concern is included.
28
+ - Gem renamed from `token_attr` to `tokenize_attr`; module namespace renamed
29
+ from `TokenAttr` to `TokenizeAttr` across all files.
30
+ - `TokenAttr::Tokenizer` extracted as a dedicated private class so that no
31
+ internal helper methods are mixed into model classes.
32
+
33
+ ## [0.1.0] - 2026-02-01
34
+
35
+ ### Added
36
+ - Initial release.
37
+ - `tokenize` class macro for ActiveRecord models.
38
+ - Transparent delegation to `has_secure_token` (Rails 5+) when no prefix or
39
+ custom generator is provided.
40
+ - Custom `before_create` callback backed by `SecureRandom.base58` when a
41
+ prefix is required or `has_secure_token` is unavailable.
42
+ - `size:` option (default 64) controlling token length.
43
+ - `prefix:` option prepending `"prefix-<token>"`.
44
+ - `retries:` option (default 3) with `TokenizeAttr::RetryExceededError` raised
45
+ when all attempts are exhausted.
46
+ - Custom generator support: proc as second positional argument, block, or
47
+ method reference via `&`.
48
+
49
+ [Unreleased]: https://github.com/pniemczyk/tokenize_attr/compare/v0.2.0...HEAD
50
+ [0.2.0]: https://github.com/pniemczyk/tokenize_attr/compare/v0.1.0...v0.2.0
51
+ [0.1.0]: https://github.com/pniemczyk/tokenize_attr/releases/tag/v0.1.0
data/CLAUDE.md CHANGED
@@ -11,3 +11,18 @@ Before writing any code, read these files in order:
11
11
  ---
12
12
 
13
13
  ## Project context
14
+
15
+ `tokenize_attr` v0.2.0 — declarative secure token generation for ActiveRecord
16
+ model attributes. See `@AGENTS.md` for the full design context.
17
+
18
+ ### Key commands
19
+
20
+ ```bash
21
+ # Run the full test suite (without Rails/Rake)
22
+ ~/.local/share/mise/installs/ruby/3.4.6/bin/ruby -Ilib:test test/test_tokenize_attr.rb
23
+ ~/.local/share/mise/installs/ruby/3.4.6/bin/ruby -Ilib:test test/tokenize_attr/installer_test.rb
24
+
25
+ # In a Rails app
26
+ rails tokenize_attr:install # create the initializer
27
+ rails tokenize_attr:uninstall # remove the initializer
28
+ ```
data/README.md CHANGED
@@ -17,6 +17,22 @@ Add to your Gemfile:
17
17
  gem "tokenize_attr"
18
18
  ```
19
19
 
20
+ Then run the install task to wire the gem into your Rails app:
21
+
22
+ ```bash
23
+ rails tokenize_attr:install
24
+ ```
25
+
26
+ This creates `config/initializers/tokenize_attr.rb`, which calls
27
+ `ActiveSupport.on_load(:active_record)` so that every model gains the
28
+ `tokenize` macro automatically.
29
+
30
+ To remove the initializer:
31
+
32
+ ```bash
33
+ rails tokenize_attr:uninstall
34
+ ```
35
+
20
36
  ## Usage
21
37
 
22
38
  ### Basic — delegates to `has_secure_token`
@@ -138,12 +154,13 @@ end
138
154
 
139
155
  ## Rails integration
140
156
 
141
- When loaded inside a Rails application `TokenizeAttr::Concern` is
142
- auto-included into `ActiveRecord::Base` via `ActiveSupport.on_load`, so
143
- every model gains the `tokenize` macro without an explicit include.
157
+ Run `rails tokenize_attr:install` once after adding the gem. The generated
158
+ initializer (`config/initializers/tokenize_attr.rb`) hooks into
159
+ `ActiveSupport.on_load(:active_record)` so every model gains the `tokenize`
160
+ macro without an explicit include.
144
161
 
145
- Outside of Rails (or with non-AR classes that provide `before_create` and
146
- `exists?`) include the concern manually:
162
+ For non-Rails classes that provide `before_create` and `exists?` include the
163
+ concern manually:
147
164
 
148
165
  ```ruby
149
166
  class MyModel
data/Rakefile CHANGED
@@ -3,7 +3,9 @@
3
3
  require "bundler/gem_tasks"
4
4
  require "minitest/test_task"
5
5
 
6
- Minitest::TestTask.create
6
+ Minitest::TestTask.create(:test) do |t|
7
+ t.test_globs = ["test/**/{test_*,*_test}.rb"]
8
+ end
7
9
 
8
10
  require "rubocop/rake_task"
9
11
 
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "tokenize_attr/installer"
4
+
5
+ namespace :tokenize_attr do
6
+ INITIALIZER_RELATIVE = TokenizeAttr::Installer::INITIALIZER_PATH.to_s
7
+
8
+ desc "Install an initializer that auto-includes TokenizeAttr::Concern into ActiveRecord"
9
+ task :install do
10
+ result = TokenizeAttr::Installer.install!(Rails.root)
11
+
12
+ case result
13
+ when :created then printf " %-10s %s\n", "create", INITIALIZER_RELATIVE
14
+ when :skipped then printf " %-10s %s\n", "skip", "#{INITIALIZER_RELATIVE} already exists"
15
+ end
16
+ end
17
+
18
+ desc "Remove the tokenize_attr initializer"
19
+ task :uninstall do
20
+ result = TokenizeAttr::Installer.uninstall!(Rails.root)
21
+
22
+ case result
23
+ when :removed then printf " %-10s %s\n", "remove", INITIALIZER_RELATIVE
24
+ when :skipped then printf " %-10s %s\n", "skip", "#{INITIALIZER_RELATIVE} not found"
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pathname"
4
+
5
+ module TokenizeAttr
6
+ # Manages the lifecycle of the tokenize_attr initializer inside a Rails app.
7
+ #
8
+ # This class holds all file-system logic so it can be unit-tested without
9
+ # requiring a full Rails boot or Rake DSL. The rake tasks are thin shells
10
+ # that delegate here.
11
+ #
12
+ # @example From a Rake task (or Rails generator):
13
+ # TokenizeAttr::Installer.install!(Rails.root)
14
+ # TokenizeAttr::Installer.uninstall!(Rails.root)
15
+ #
16
+ class Installer
17
+ INITIALIZER_PATH = Pathname.new("config/initializers/tokenize_attr.rb")
18
+
19
+ INITIALIZER_CONTENT = <<~RUBY
20
+ # frozen_string_literal: true
21
+
22
+ # Auto-include TokenizeAttr::Concern into every ActiveRecord model so that
23
+ # the .tokenize macro is available without an explicit include in each class.
24
+ #
25
+ # Generated by: rails tokenize_attr:install
26
+ #
27
+ # To opt out of auto-include, remove this file and add
28
+ # include TokenizeAttr::Concern
29
+ # to whichever models need it.
30
+ ActiveSupport.on_load(:active_record) do
31
+ include TokenizeAttr::Concern
32
+ end
33
+ RUBY
34
+
35
+ # Write the initializer to +rails_root/config/initializers/tokenize_attr.rb+.
36
+ #
37
+ # @param rails_root [Pathname, String] the root directory of the Rails app
38
+ # @return [:created, :skipped]
39
+ def self.install!(rails_root)
40
+ destination = Pathname.new(rails_root).join(INITIALIZER_PATH)
41
+
42
+ return :skipped if destination.exist?
43
+
44
+ destination.write(INITIALIZER_CONTENT)
45
+ :created
46
+ end
47
+
48
+ # Remove the initializer if it exists.
49
+ #
50
+ # @param rails_root [Pathname, String]
51
+ # @return [:removed, :skipped]
52
+ def self.uninstall!(rails_root)
53
+ destination = Pathname.new(rails_root).join(INITIALIZER_PATH)
54
+
55
+ return :skipped unless destination.exist?
56
+
57
+ destination.delete
58
+ :removed
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TokenizeAttr
4
+ class Railtie < Rails::Railtie
5
+ railtie_name :tokenize_attr
6
+
7
+ # Expose `rails tokenize_attr:install` and `rails tokenize_attr:uninstall`
8
+ # to the host application.
9
+ rake_tasks do
10
+ load File.expand_path("../tasks/tokenize_attr.rake", __dir__)
11
+ end
12
+ end
13
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module TokenizeAttr
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
data/lib/tokenize_attr.rb CHANGED
@@ -8,11 +8,28 @@ require_relative "tokenize_attr/version"
8
8
  require_relative "tokenize_attr/errors"
9
9
  require_relative "tokenize_attr/tokenizer"
10
10
  require_relative "tokenize_attr/concern"
11
+ require_relative "tokenize_attr/installer"
11
12
 
13
+ # TokenizeAttr adds a declarative +tokenize+ class macro to ActiveRecord models.
14
+ #
15
+ # In a Rails app, run the install task to wire the gem in:
16
+ #
17
+ # rails tokenize_attr:install
18
+ #
19
+ # That creates +config/initializers/tokenize_attr.rb+ which calls
20
+ # +ActiveSupport.on_load(:active_record)+ so that every ActiveRecord model
21
+ # gets the +tokenize+ macro without an explicit include.
22
+ #
23
+ # For non-Rails classes that provide +before_create+ and +exists?+, include
24
+ # the concern manually:
25
+ #
26
+ # class MyModel
27
+ # include TokenizeAttr::Concern
28
+ # end
29
+ #
12
30
  module TokenizeAttr
13
- # When loaded inside a Rails application, auto-include the concern into
14
- # ActiveRecord::Base so every model gains the +tokenize+ macro without
15
- # an explicit include. If ActiveRecord is already loaded (e.g. in tests
16
- # without a full Rails boot) the block executes immediately.
17
- ActiveSupport.on_load(:active_record) { include TokenizeAttr::Concern }
18
31
  end
32
+
33
+ # Register the Railtie when running inside a Rails application.
34
+ # The Railtie exposes the `rails tokenize_attr:install` rake task.
35
+ require "tokenize_attr/railtie" if defined?(Rails::Railtie)
data/llms/overview.md CHANGED
@@ -27,6 +27,37 @@ tokenize(attribute, generator = nil, size: 64, prefix: nil, retries: 3, &block)
27
27
 
28
28
  ---
29
29
 
30
+ ## Rails installation
31
+
32
+ `tokenize_attr` does not auto-include itself at require time. In a Rails app,
33
+ run once after adding the gem to the Gemfile:
34
+
35
+ ```bash
36
+ rails tokenize_attr:install
37
+ ```
38
+
39
+ This creates `config/initializers/tokenize_attr.rb`:
40
+
41
+ ```ruby
42
+ # frozen_string_literal: true
43
+ ActiveSupport.on_load(:active_record) do
44
+ include TokenizeAttr::Concern
45
+ end
46
+ ```
47
+
48
+ To remove:
49
+
50
+ ```bash
51
+ rails tokenize_attr:uninstall
52
+ ```
53
+
54
+ The Railtie (`TokenizeAttr::Railtie`) is loaded automatically when
55
+ `Rails::Railtie` is defined (i.e. inside a Rails app). In plain
56
+ `activerecord` contexts (tests, scripts) include the concern manually or
57
+ call `ActiveSupport.on_load` directly, as the test helper does.
58
+
59
+ ---
60
+
30
61
  ## Internal decision tree
31
62
 
32
63
  All logic below lives in `TokenizeAttr::Tokenizer` (see
@@ -57,6 +88,24 @@ tokenize called → TokenizeAttr::Tokenizer.apply(klass, attribute, ...)
57
88
 
58
89
  ---
59
90
 
91
+ ## Installer class
92
+
93
+ `TokenizeAttr::Installer` is a plain Ruby class (no Rake or Rails dependency)
94
+ that handles the filesystem operations for the initializer lifecycle.
95
+
96
+ ```
97
+ TokenizeAttr::Installer
98
+ .install!(rails_root) → :created | :skipped
99
+ .uninstall!(rails_root) → :removed | :skipped
100
+
101
+ INITIALIZER_PATH = Pathname("config/initializers/tokenize_attr.rb")
102
+ ```
103
+
104
+ `TokenizeAttr::Railtie` loads the rake task file and delegates to
105
+ `Installer` from within the `task :install` and `task :uninstall` blocks.
106
+
107
+ ---
108
+
60
109
  ## Error class hierarchy
61
110
 
62
111
  ```
@@ -70,10 +119,13 @@ StandardError
70
119
  ## Key constraints
71
120
 
72
121
  - Only `activesupport` is a hard runtime dependency.
73
- - `ActiveRecord::Base` receives the concern via `ActiveSupport.on_load(:active_record)`.
122
+ - `ActiveRecord::Base` receives the concern via the generated initializer
123
+ (`rails tokenize_attr:install`), not at require time.
74
124
  - All generation helpers live in `TokenizeAttr::Tokenizer`, not in the model
75
125
  class. This prevents method-name collisions on user models. Only the
76
126
  `tokenize` macro is mixed in via `Concern`.
127
+ - `TokenizeAttr::Installer` must stay dependency-free (no Rake, no Rails) so
128
+ it can be unit-tested without a Rails boot.
77
129
  - Tests use bare `activerecord` + SQLite3 — no `rails` gem.
78
130
  - `has_secure_token` delegation is skipped when `prefix:` is provided
79
131
  because that built-in has no prefix support.
data/llms/usage.md CHANGED
@@ -4,6 +4,23 @@
4
4
 
5
5
  ---
6
6
 
7
+ ## Installation in a Rails app
8
+
9
+ ```bash
10
+ rails tokenize_attr:install # creates config/initializers/tokenize_attr.rb
11
+ rails tokenize_attr:uninstall # removes the initializer
12
+ ```
13
+
14
+ The generated initializer wires the gem into ActiveRecord:
15
+
16
+ ```ruby
17
+ ActiveSupport.on_load(:active_record) do
18
+ include TokenizeAttr::Concern
19
+ end
20
+ ```
21
+
22
+ ---
23
+
7
24
  ## Basic (no prefix) — delegates to `has_secure_token`
8
25
 
9
26
  ```ruby
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tokenize_attr
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Pawel Niemczyk
@@ -42,13 +42,17 @@ extensions: []
42
42
  extra_rdoc_files: []
43
43
  files:
44
44
  - AGENTS.md
45
+ - CHANGELOG.md
45
46
  - CLAUDE.md
46
47
  - LICENSE.txt
47
48
  - README.md
48
49
  - Rakefile
50
+ - lib/tasks/tokenize_attr.rake
49
51
  - lib/tokenize_attr.rb
50
52
  - lib/tokenize_attr/concern.rb
51
53
  - lib/tokenize_attr/errors.rb
54
+ - lib/tokenize_attr/installer.rb
55
+ - lib/tokenize_attr/railtie.rb
52
56
  - lib/tokenize_attr/tokenizer.rb
53
57
  - lib/tokenize_attr/version.rb
54
58
  - llms/overview.md