tokenize_attr 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/AGENTS.md +110 -0
- data/CLAUDE.md +13 -0
- data/LICENSE.txt +21 -0
- data/README.md +180 -0
- data/Rakefile +12 -0
- data/lib/tokenize_attr/concern.rb +83 -0
- data/lib/tokenize_attr/errors.rb +23 -0
- data/lib/tokenize_attr/tokenizer.rb +83 -0
- data/lib/tokenize_attr/version.rb +5 -0
- data/lib/tokenize_attr.rb +18 -0
- data/llms/overview.md +81 -0
- data/llms/usage.md +181 -0
- data/sig/tokenize_attr.rbs +35 -0
- metadata +80 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: abf1c8d9447a7b940dca5bd4759bf60c82ffb967dfcffa18d5df14b41512cfe2
|
|
4
|
+
data.tar.gz: c88d3236ac9fae09d5c9d879f4ac0baba43df11ebe8408db5d039fae58b2bd6c
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: f50afa2586d8689212b18a1577da21b372cf499fb1022279475ed609adc0dea4ddcfa712d7ecaa6d70a9fba38732886b68445737f82f8541cd0cf8ba620da4b5
|
|
7
|
+
data.tar.gz: d6e087c763c99973351e884a3cb81b7a13527b04e8664cbe15946fbb1bb77d7fe5bec4a49dfc1827271206f827681c68c3ca782f4386a3a0b76e023edee827b0
|
data/AGENTS.md
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# AGENTS.md — tokenize_attr
|
|
2
|
+
|
|
3
|
+
This file is the primary context document for AI agents and LLMs working on
|
|
4
|
+
this gem. Read it fully before making any changes.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## What this gem does
|
|
9
|
+
|
|
10
|
+
`tokenize_attr` provides a single public API — the `tokenize` class macro —
|
|
11
|
+
available on any ActiveRecord model (or any class with `before_create` and
|
|
12
|
+
a class-level `exists?` method).
|
|
13
|
+
|
|
14
|
+
**When no prefix and no generator are given** and the including class responds
|
|
15
|
+
to `has_secure_token` (Rails 5+), `tokenize` delegates entirely to that
|
|
16
|
+
built-in. This gives Rails-idiomatic token generation for free and avoids
|
|
17
|
+
duplicating Rails core behaviour.
|
|
18
|
+
|
|
19
|
+
**When a prefix or a custom generator is given** (or `has_secure_token` is
|
|
20
|
+
unavailable), `tokenize` installs a `before_create` callback that:
|
|
21
|
+
|
|
22
|
+
1. Skips if the attribute already has a value (`present?` check).
|
|
23
|
+
2. Calls `generator.call(size)` if a generator was provided, otherwise calls
|
|
24
|
+
`SecureRandom.base58(size)` to produce the random portion.
|
|
25
|
+
3. Prepends `prefix-` to the random portion when `prefix:` is set.
|
|
26
|
+
4. Checks uniqueness via `self.class.exists?(attribute => candidate)`.
|
|
27
|
+
5. Retries up to `retries` times (default 3).
|
|
28
|
+
6. Raises `TokenizeAttr::RetryExceededError` if all retries are exhausted.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## File map
|
|
33
|
+
|
|
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. |
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Design decisions
|
|
50
|
+
|
|
51
|
+
### Why delegate to `has_secure_token`?
|
|
52
|
+
Rails' `has_secure_token` is battle-tested and available in every Rails 5+
|
|
53
|
+
app. Delegating when possible avoids reimplementing core Rails behaviour and
|
|
54
|
+
ensures compatibility with any `regenerate_<attr>` helpers Rails adds.
|
|
55
|
+
|
|
56
|
+
### Why `SecureRandom.base58`?
|
|
57
|
+
Base58 produces URL-safe strings without ambiguous characters (no `0`, `O`,
|
|
58
|
+
`I`, `l`). Combined with a meaningful prefix it creates tokens that are both
|
|
59
|
+
human-readable and collision-resistant.
|
|
60
|
+
|
|
61
|
+
### Why configurable `retries` with an explicit error?
|
|
62
|
+
Silent token-generation failures are worse than loud ones. A configurable
|
|
63
|
+
retry budget with an explicit error makes the failure mode observable and
|
|
64
|
+
actionable. The default of 3 is generous given the token space size.
|
|
65
|
+
|
|
66
|
+
### Why is `retries` ignored when delegating to `has_secure_token`?
|
|
67
|
+
Rails does not perform uniqueness checks in `has_secure_token`; it relies on
|
|
68
|
+
the DB constraint and the astronomically large token space. Retrying there
|
|
69
|
+
would be misleading. Document the discrepancy clearly.
|
|
70
|
+
|
|
71
|
+
### Why does a generator always bypass `has_secure_token`?
|
|
72
|
+
The user has explicitly chosen a custom algorithm. Delegating to
|
|
73
|
+
`has_secure_token` anyway would silently ignore the generator, which is
|
|
74
|
+
surprising. Presence of a generator always routes through the callback path.
|
|
75
|
+
|
|
76
|
+
### Why is `TokenizeAttr::Tokenizer` a separate class?
|
|
77
|
+
Keeping all generation helpers in `Tokenizer` rather than in the
|
|
78
|
+
`class_methods` block of `Concern` ensures that none of those methods are
|
|
79
|
+
mixed into the user's model class. This eliminates any risk of method-name
|
|
80
|
+
collisions for models that happen to define methods with similar names.
|
|
81
|
+
`Concern` only exposes the single public `tokenize` macro.
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## Guardrails
|
|
86
|
+
|
|
87
|
+
- **Do not change the `tokenize` signature** without a major version bump.
|
|
88
|
+
- **Do not add a hard runtime dependency on `activerecord`**. Only
|
|
89
|
+
`activesupport` is required. AR's `before_create` and `exists?` are
|
|
90
|
+
consumed via duck typing.
|
|
91
|
+
- **Keep tests without the `rails` gem**. The suite uses bare `activerecord`
|
|
92
|
+
+ in-memory SQLite — no full Rails boot.
|
|
93
|
+
- **Do not swallow `RetryExceededError`**. It is intentionally raised so
|
|
94
|
+
callers can handle it.
|
|
95
|
+
- **Keep `TokenizeAttr::Tokenizer` methods private** (except `apply`). The
|
|
96
|
+
class is `@api private`; only `apply` is the stable internal contract.
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## Test conventions
|
|
101
|
+
|
|
102
|
+
- One `Minitest::Test` subclass per behavioural group.
|
|
103
|
+
- `setup` truncates the shared `records` table via a raw SQL DELETE.
|
|
104
|
+
- Collision scenarios are tested using **anonymous subclasses** that override
|
|
105
|
+
`exists?` directly (e.g. `Class.new(ApiClient) { def self.exists?(*) = true }`).
|
|
106
|
+
Do **not** use minitest's `stub` on AR model classes — ActiveRecord 8.x
|
|
107
|
+
`method_missing` intercepts `stub` before minitest can install it.
|
|
108
|
+
- Model classes share `self.table_name = "records"` so the schema stays flat.
|
|
109
|
+
- All column names used by test models must exist in the `records` table
|
|
110
|
+
defined in `test/test_helper.rb`.
|
data/CLAUDE.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# CLAUDE.md — tokenize_attr
|
|
2
|
+
|
|
3
|
+
## Start here
|
|
4
|
+
|
|
5
|
+
Before writing any code, read these files in order:
|
|
6
|
+
|
|
7
|
+
1. **@AGENTS.md** — architecture, design decisions, guardrails, test conventions
|
|
8
|
+
2. **@llms/overview.md** — class responsibilities and internal design notes
|
|
9
|
+
3. **@llms/usage.md** — common patterns and recipes
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Project context
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Pawel Niemczyk
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# tokenize_attr
|
|
2
|
+
|
|
3
|
+
Declarative secure token generation for ActiveRecord model attributes.
|
|
4
|
+
|
|
5
|
+
`tokenize_attr` adds a `tokenize` class macro to ActiveRecord models. When no
|
|
6
|
+
prefix is needed it transparently delegates to Rails' built-in
|
|
7
|
+
`has_secure_token` (Rails 5+). When a prefix is required — or when
|
|
8
|
+
`has_secure_token` is unavailable — it installs a `before_create` callback
|
|
9
|
+
backed by `SecureRandom.base58` with configurable size, prefix, and retry
|
|
10
|
+
logic.
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
Add to your Gemfile:
|
|
15
|
+
|
|
16
|
+
```ruby
|
|
17
|
+
gem "tokenize_attr"
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
### Basic — delegates to `has_secure_token`
|
|
23
|
+
|
|
24
|
+
When no prefix is provided and the model supports `has_secure_token` (every
|
|
25
|
+
Rails 5+ `ApplicationRecord`), the built-in Rails implementation is used
|
|
26
|
+
automatically.
|
|
27
|
+
|
|
28
|
+
```ruby
|
|
29
|
+
class User < ApplicationRecord
|
|
30
|
+
tokenize :api_token
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
user = User.create!
|
|
34
|
+
user.api_token # => "aBcD1234..." (64 base58 chars by default)
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### With prefix
|
|
38
|
+
|
|
39
|
+
When a `prefix:` is given the gem installs its own `before_create` callback.
|
|
40
|
+
|
|
41
|
+
```ruby
|
|
42
|
+
class AccessToken < ApplicationRecord
|
|
43
|
+
tokenize :token, prefix: "tok", size: 32
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
AccessToken.create!.token # => "tok-aBcD1234..." ("tok-" + 32 random chars)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Custom size (no prefix)
|
|
50
|
+
|
|
51
|
+
```ruby
|
|
52
|
+
class Session < ApplicationRecord
|
|
53
|
+
tokenize :session_id, size: 128
|
|
54
|
+
end
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Custom retry budget
|
|
58
|
+
|
|
59
|
+
```ruby
|
|
60
|
+
class InviteCode < ApplicationRecord
|
|
61
|
+
# Retry up to 5 times before raising TokenizeAttr::RetryExceededError
|
|
62
|
+
tokenize :code, prefix: "inv", retries: 5
|
|
63
|
+
end
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Custom generator
|
|
67
|
+
|
|
68
|
+
Pass any callable as the second argument (or as a block) to replace
|
|
69
|
+
`SecureRandom.base58` with your own algorithm. The callable receives `size`
|
|
70
|
+
and must return a String.
|
|
71
|
+
|
|
72
|
+
```ruby
|
|
73
|
+
# Proc as second positional argument
|
|
74
|
+
class Order < ApplicationRecord
|
|
75
|
+
tokenize :reference, proc { |size| SecureRandom.alphanumeric(size) },
|
|
76
|
+
prefix: "ord", size: 12
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
Order.create!.reference # => "ord-aB3cD4eF5gH6"
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
```ruby
|
|
83
|
+
# Method reference via &
|
|
84
|
+
class Order < ApplicationRecord
|
|
85
|
+
def self.reference_generator(size) = SecureRandom.alphanumeric(size)
|
|
86
|
+
tokenize :reference, &method(:reference_generator)
|
|
87
|
+
end
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
```ruby
|
|
91
|
+
# Inline block (parentheses required)
|
|
92
|
+
class Order < ApplicationRecord
|
|
93
|
+
tokenize(:reference) { |size| SecureRandom.alphanumeric(size) }
|
|
94
|
+
end
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
> **Note:** Providing a generator always uses the callback path — even when
|
|
98
|
+
> no `prefix:` is given. The generator takes precedence over
|
|
99
|
+
> `has_secure_token`.
|
|
100
|
+
|
|
101
|
+
### Multiple tokenized attributes
|
|
102
|
+
|
|
103
|
+
```ruby
|
|
104
|
+
class ApiCredential < ApplicationRecord
|
|
105
|
+
tokenize :public_key, prefix: "pk", size: 32
|
|
106
|
+
tokenize :private_key, prefix: "sk", size: 64
|
|
107
|
+
end
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Options
|
|
111
|
+
|
|
112
|
+
| Option | Default | Description |
|
|
113
|
+
|-------------|---------|------------------------------------------------------------------|
|
|
114
|
+
| `generator` | `nil` | Proc/block called as `generator.call(size)` → String. |
|
|
115
|
+
| | | Overrides the default `SecureRandom.base58` algorithm. |
|
|
116
|
+
| `size` | `64` | Length passed to the generator or to `SecureRandom.base58`. |
|
|
117
|
+
| `prefix` | `nil` | String prepended as `"prefix-<token>"`. |
|
|
118
|
+
| `retries` | `3` | Max uniqueness-check attempts (custom callback path only). |
|
|
119
|
+
|
|
120
|
+
> **Note:** `retries` is ignored when delegating to `has_secure_token`
|
|
121
|
+
> because Rails does not perform uniqueness checks there. Use a DB unique
|
|
122
|
+
> index to enforce uniqueness and let the DB raise on collision.
|
|
123
|
+
|
|
124
|
+
## Error handling
|
|
125
|
+
|
|
126
|
+
When the custom callback exhausts all retry attempts it raises
|
|
127
|
+
`TokenizeAttr::RetryExceededError` (a subclass of `TokenizeAttr::Error <
|
|
128
|
+
StandardError`).
|
|
129
|
+
|
|
130
|
+
```ruby
|
|
131
|
+
begin
|
|
132
|
+
InviteCode.create!
|
|
133
|
+
rescue TokenizeAttr::RetryExceededError => e
|
|
134
|
+
Rails.logger.error(e.message)
|
|
135
|
+
# => "Could not generate a unique token for :code after 5 retries"
|
|
136
|
+
end
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Rails integration
|
|
140
|
+
|
|
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.
|
|
144
|
+
|
|
145
|
+
Outside of Rails (or with non-AR classes that provide `before_create` and
|
|
146
|
+
`exists?`) include the concern manually:
|
|
147
|
+
|
|
148
|
+
```ruby
|
|
149
|
+
class MyModel
|
|
150
|
+
include TokenizeAttr::Concern
|
|
151
|
+
end
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Recommended migration
|
|
155
|
+
|
|
156
|
+
Add a unique index on tokenized columns to enforce uniqueness at the DB
|
|
157
|
+
level:
|
|
158
|
+
|
|
159
|
+
```ruby
|
|
160
|
+
add_column :access_tokens, :token, :string
|
|
161
|
+
add_index :access_tokens, :token, unique: true
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Development
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
bin/setup # install dependencies
|
|
168
|
+
bundle exec rake test # run the test suite
|
|
169
|
+
bin/console # interactive prompt
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## Contributing
|
|
173
|
+
|
|
174
|
+
Bug reports and pull requests are welcome on GitHub at
|
|
175
|
+
https://github.com/pniemczyk/tokenize_attr.
|
|
176
|
+
|
|
177
|
+
## License
|
|
178
|
+
|
|
179
|
+
The gem is available as open source under the terms of the
|
|
180
|
+
[MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TokenizeAttr
|
|
4
|
+
# Include this concern in any ActiveRecord model (or any class that
|
|
5
|
+
# provides +before_create+ and a class-level +exists?+ predicate) to gain
|
|
6
|
+
# the +tokenize+ class macro.
|
|
7
|
+
#
|
|
8
|
+
# When Rails is loaded the concern is auto-included into
|
|
9
|
+
# +ActiveRecord::Base+ via an +ActiveSupport.on_load+ hook, so explicit
|
|
10
|
+
# inclusion is not needed in Rails apps.
|
|
11
|
+
#
|
|
12
|
+
# All generation logic is delegated to +TokenizeAttr::Tokenizer+ so that no
|
|
13
|
+
# internal helpers are mixed into the model class.
|
|
14
|
+
module Concern
|
|
15
|
+
extend ActiveSupport::Concern
|
|
16
|
+
|
|
17
|
+
class_methods do
|
|
18
|
+
# Configures secure token generation for +attribute+.
|
|
19
|
+
#
|
|
20
|
+
# When +prefix+ is +nil+ *and* no +generator+ is given *and* the
|
|
21
|
+
# including class responds to +has_secure_token+ (Rails 5+), the
|
|
22
|
+
# Rails built-in is used. Note: +retries+ is ignored in that path
|
|
23
|
+
# because +has_secure_token+ relies on DB constraints rather than
|
|
24
|
+
# an application-level uniqueness check.
|
|
25
|
+
#
|
|
26
|
+
# Otherwise a custom +before_create+ callback is installed. All
|
|
27
|
+
# implementation details live in +TokenizeAttr::Tokenizer+.
|
|
28
|
+
#
|
|
29
|
+
# @param attribute [Symbol] the attribute to assign the token to
|
|
30
|
+
# @param generator [Proc, nil] optional callable invoked as
|
|
31
|
+
# +generator.call(size)+ to produce the random portion of the token.
|
|
32
|
+
# May also be supplied as a block. When present the custom callback
|
|
33
|
+
# path is always used, even when +has_secure_token+ is available.
|
|
34
|
+
# @param size [Integer] length passed to the generator or
|
|
35
|
+
# to +SecureRandom.base58+ (default 64)
|
|
36
|
+
# @param prefix [String, nil] optional prefix, joined as
|
|
37
|
+
# +"prefix-<token>"+
|
|
38
|
+
# @param retries [Integer] max uniqueness-check retries
|
|
39
|
+
# (default 3); ignored on the +has_secure_token+ path
|
|
40
|
+
#
|
|
41
|
+
# @raise [TokenizeAttr::RetryExceededError] when uniqueness cannot be
|
|
42
|
+
# established within the retry budget
|
|
43
|
+
#
|
|
44
|
+
# @example Delegate to has_secure_token (no prefix, no generator)
|
|
45
|
+
# class User < ApplicationRecord
|
|
46
|
+
# tokenize :api_token
|
|
47
|
+
# end
|
|
48
|
+
#
|
|
49
|
+
# @example Custom callback with prefix
|
|
50
|
+
# class AccessToken < ApplicationRecord
|
|
51
|
+
# tokenize :token, size: 32, prefix: "tok"
|
|
52
|
+
# end
|
|
53
|
+
#
|
|
54
|
+
# AccessToken.create!.token #=> "tok-aBcD1234..."
|
|
55
|
+
#
|
|
56
|
+
# @example Proc passed as second argument
|
|
57
|
+
# class Order < ApplicationRecord
|
|
58
|
+
# tokenize :reference, proc { |size| SecureRandom.alphanumeric(size) },
|
|
59
|
+
# prefix: "ord", size: 12
|
|
60
|
+
# end
|
|
61
|
+
#
|
|
62
|
+
# @example Method reference via &
|
|
63
|
+
# class Order < ApplicationRecord
|
|
64
|
+
# def self.reference_generator(size) = SecureRandom.alphanumeric(size)
|
|
65
|
+
# tokenize :reference, &method(:reference_generator)
|
|
66
|
+
# end
|
|
67
|
+
#
|
|
68
|
+
# @example Inline block
|
|
69
|
+
# class Order < ApplicationRecord
|
|
70
|
+
# tokenize(:reference) { |size| SecureRandom.alphanumeric(size) }
|
|
71
|
+
# end
|
|
72
|
+
def tokenize(attribute, generator = nil, size: 64, prefix: nil, retries: 3, &block)
|
|
73
|
+
TokenizeAttr::Tokenizer.apply(
|
|
74
|
+
self, attribute,
|
|
75
|
+
generator: generator || block,
|
|
76
|
+
size: size,
|
|
77
|
+
prefix: prefix,
|
|
78
|
+
retries: retries
|
|
79
|
+
)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TokenizeAttr
|
|
4
|
+
# Base error class for all tokenize_attr exceptions.
|
|
5
|
+
class Error < StandardError; end
|
|
6
|
+
|
|
7
|
+
# Raised when a unique token cannot be generated within the configured
|
|
8
|
+
# number of retries.
|
|
9
|
+
#
|
|
10
|
+
# @example
|
|
11
|
+
# begin
|
|
12
|
+
# MyModel.create!
|
|
13
|
+
# rescue TokenizeAttr::RetryExceededError => e
|
|
14
|
+
# Rails.logger.error(e.message)
|
|
15
|
+
# end
|
|
16
|
+
class RetryExceededError < Error
|
|
17
|
+
# @param attribute [Symbol, String] the attribute that failed to tokenize
|
|
18
|
+
# @param retries [Integer] the number of attempts that were made
|
|
19
|
+
def initialize(attribute, retries)
|
|
20
|
+
super("Could not generate a unique token for :#{attribute} after #{retries} retries")
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TokenizeAttr
|
|
4
|
+
# Internal class that holds all token-generation logic.
|
|
5
|
+
#
|
|
6
|
+
# Keeping this logic here — rather than inside +TokenizeAttr::Concern+'s
|
|
7
|
+
# +class_methods+ block — ensures that none of these methods are mixed
|
|
8
|
+
# into the including model class, eliminating any risk of method-name
|
|
9
|
+
# collisions on user models.
|
|
10
|
+
#
|
|
11
|
+
# @api private
|
|
12
|
+
class Tokenizer
|
|
13
|
+
class << self
|
|
14
|
+
# Determines which generation strategy to use and configures +klass+.
|
|
15
|
+
#
|
|
16
|
+
# Routes to +has_secure_token+ when all of the following are true:
|
|
17
|
+
# - +generator+ is +nil+
|
|
18
|
+
# - +prefix+ is +nil+
|
|
19
|
+
# - +klass+ responds to +has_secure_token+ (Rails 5+)
|
|
20
|
+
#
|
|
21
|
+
# Otherwise installs a custom +before_create+ callback.
|
|
22
|
+
#
|
|
23
|
+
# @param klass [Class] the model class to configure
|
|
24
|
+
# @param attribute [Symbol] the attribute to assign the token to
|
|
25
|
+
# @param generator [Proc, nil] optional callable invoked as
|
|
26
|
+
# +generator.call(size)+ to produce the random portion
|
|
27
|
+
# @param size [Integer] length passed to the generator or to
|
|
28
|
+
# +SecureRandom.base58+
|
|
29
|
+
# @param prefix [String, nil] optional prefix, joined as
|
|
30
|
+
# +"prefix-<token>"+
|
|
31
|
+
# @param retries [Integer] max uniqueness-check attempts;
|
|
32
|
+
# ignored on the +has_secure_token+ path
|
|
33
|
+
def apply(klass, attribute, generator:, size:, prefix:, retries:) # rubocop:disable Metrics/ParameterLists
|
|
34
|
+
if generator.nil? && prefix.nil? && has_secure_token?(klass)
|
|
35
|
+
via_has_secure_token(klass, attribute, size)
|
|
36
|
+
else
|
|
37
|
+
via_callback(klass, attribute, size: size, prefix: prefix,
|
|
38
|
+
retries: retries, generator: generator)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def has_secure_token?(klass) # rubocop:disable Naming/PredicatePrefix
|
|
45
|
+
klass.respond_to?(:has_secure_token)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Delegates to Rails' has_secure_token. Passes +length:+ when supported
|
|
49
|
+
# (Rails 6.1+); falls back silently on older Rails versions.
|
|
50
|
+
def via_has_secure_token(klass, attribute, size)
|
|
51
|
+
klass.has_secure_token(attribute, length: size)
|
|
52
|
+
rescue ArgumentError
|
|
53
|
+
klass.has_secure_token(attribute)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Installs a +before_create+ callback for custom token generation.
|
|
57
|
+
#
|
|
58
|
+
# When +generator+ is provided it is called as +generator.call(size)+
|
|
59
|
+
# to produce the random portion; otherwise +SecureRandom.base58(size)+
|
|
60
|
+
# is used.
|
|
61
|
+
def via_callback(klass, attribute, size:, prefix:, retries:, generator: nil) # rubocop:disable Metrics/MethodLength, Metrics/ParameterLists
|
|
62
|
+
klass.before_create do
|
|
63
|
+
next if send(attribute).present?
|
|
64
|
+
|
|
65
|
+
token_generated = false
|
|
66
|
+
|
|
67
|
+
retries.times do
|
|
68
|
+
random_part = generator ? generator.call(size) : SecureRandom.base58(size)
|
|
69
|
+
candidate = [prefix, random_part].compact.join("-")
|
|
70
|
+
send(:"#{attribute}=", candidate)
|
|
71
|
+
|
|
72
|
+
unless self.class.exists?(attribute => candidate)
|
|
73
|
+
token_generated = true
|
|
74
|
+
break
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
raise TokenizeAttr::RetryExceededError.new(attribute, retries) unless token_generated
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support"
|
|
4
|
+
require "active_support/concern"
|
|
5
|
+
require "securerandom"
|
|
6
|
+
|
|
7
|
+
require_relative "tokenize_attr/version"
|
|
8
|
+
require_relative "tokenize_attr/errors"
|
|
9
|
+
require_relative "tokenize_attr/tokenizer"
|
|
10
|
+
require_relative "tokenize_attr/concern"
|
|
11
|
+
|
|
12
|
+
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
|
+
end
|
data/llms/overview.md
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# tokenize_attr — LLM Context Overview
|
|
2
|
+
|
|
3
|
+
> Load this file before modifying or extending the gem.
|
|
4
|
+
|
|
5
|
+
## Purpose
|
|
6
|
+
|
|
7
|
+
`tokenize_attr` is a Ruby gem that provides declarative secure token generation
|
|
8
|
+
for ActiveRecord model attributes via a single `tokenize` class macro.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Public API
|
|
13
|
+
|
|
14
|
+
```ruby
|
|
15
|
+
# Full signature
|
|
16
|
+
tokenize(attribute, generator = nil, size: 64, prefix: nil, retries: 3, &block)
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
| Param | Type | Default | Notes |
|
|
20
|
+
|-------------|------------------|---------|----------------------------------------------------|
|
|
21
|
+
| `attribute` | Symbol | — | The attribute to tokenize |
|
|
22
|
+
| `generator` | Proc / nil | nil | Called as `generator.call(size)` for random part. |
|
|
23
|
+
| | | | May also be supplied as a `&block`. |
|
|
24
|
+
| `size` | Integer | 64 | Passed to generator or to `SecureRandom.base58` |
|
|
25
|
+
| `prefix` | String / nil | nil | Prepended as `"prefix-<token>"` |
|
|
26
|
+
| `retries` | Integer | 3 | Max uniqueness-check attempts (callback path only) |
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Internal decision tree
|
|
31
|
+
|
|
32
|
+
All logic below lives in `TokenizeAttr::Tokenizer` (see
|
|
33
|
+
`lib/tokenize_attr/tokenizer.rb`). `TokenizeAttr::Concern#tokenize` is a thin
|
|
34
|
+
delegator that calls `TokenizeAttr::Tokenizer.apply`.
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
tokenize called → TokenizeAttr::Tokenizer.apply(klass, attribute, ...)
|
|
38
|
+
└─ generator nil? AND prefix nil? AND has_secure_token available?
|
|
39
|
+
├─ YES → Tokenizer.via_has_secure_token(klass, attribute, size)
|
|
40
|
+
│ tries: klass.has_secure_token(attribute, length: size)
|
|
41
|
+
│ rescue ArgumentError → klass.has_secure_token(attribute)
|
|
42
|
+
│ (retries param is IGNORED on this path)
|
|
43
|
+
│
|
|
44
|
+
└─ NO → Tokenizer.via_callback(klass, attribute, size:, prefix:, retries:, generator:)
|
|
45
|
+
klass.before_create:
|
|
46
|
+
attribute.present? → skip
|
|
47
|
+
loop retries times:
|
|
48
|
+
random_part = generator ? generator.call(size)
|
|
49
|
+
: SecureRandom.base58(size)
|
|
50
|
+
candidate = [prefix, random_part].compact.join("-")
|
|
51
|
+
assign candidate to attribute
|
|
52
|
+
exists?(attribute => candidate)? → next iteration
|
|
53
|
+
else → token_generated = true; break
|
|
54
|
+
token_generated? → done
|
|
55
|
+
else → raise RetryExceededError
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Error class hierarchy
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
StandardError
|
|
64
|
+
└── TokenizeAttr::Error
|
|
65
|
+
└── TokenizeAttr::RetryExceededError
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## Key constraints
|
|
71
|
+
|
|
72
|
+
- Only `activesupport` is a hard runtime dependency.
|
|
73
|
+
- `ActiveRecord::Base` receives the concern via `ActiveSupport.on_load(:active_record)`.
|
|
74
|
+
- All generation helpers live in `TokenizeAttr::Tokenizer`, not in the model
|
|
75
|
+
class. This prevents method-name collisions on user models. Only the
|
|
76
|
+
`tokenize` macro is mixed in via `Concern`.
|
|
77
|
+
- Tests use bare `activerecord` + SQLite3 — no `rails` gem.
|
|
78
|
+
- `has_secure_token` delegation is skipped when `prefix:` is provided
|
|
79
|
+
because that built-in has no prefix support.
|
|
80
|
+
- `has_secure_token` delegation is also skipped when a `generator` is
|
|
81
|
+
provided — the generator always routes through the callback path.
|
data/llms/usage.md
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# tokenize_attr — Usage Patterns
|
|
2
|
+
|
|
3
|
+
> Common patterns and recipes for LLMs helping users work with this gem.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Basic (no prefix) — delegates to `has_secure_token`
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
class User < ApplicationRecord
|
|
11
|
+
tokenize :api_token
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
user = User.create!
|
|
15
|
+
user.api_token # => "aBcD1234..." (64 base58 chars)
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
`has_secure_token` is used automatically. `retries` is ignored on this path.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## With prefix — uses custom callback
|
|
23
|
+
|
|
24
|
+
```ruby
|
|
25
|
+
class AccessToken < ApplicationRecord
|
|
26
|
+
tokenize :token, prefix: "tok", size: 32
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
token = AccessToken.create!
|
|
30
|
+
token.token # => "tok-aBcD1234..." ("tok-" + 32 random chars)
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Custom size (no prefix)
|
|
36
|
+
|
|
37
|
+
```ruby
|
|
38
|
+
class Session < ApplicationRecord
|
|
39
|
+
tokenize :session_id, size: 128
|
|
40
|
+
end
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Custom retry budget
|
|
46
|
+
|
|
47
|
+
```ruby
|
|
48
|
+
class InviteCode < ApplicationRecord
|
|
49
|
+
tokenize :code, prefix: "inv", retries: 5
|
|
50
|
+
end
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Handling `RetryExceededError`
|
|
56
|
+
|
|
57
|
+
```ruby
|
|
58
|
+
begin
|
|
59
|
+
InviteCode.create!
|
|
60
|
+
rescue TokenizeAttr::RetryExceededError => e
|
|
61
|
+
Rails.logger.error("Token generation failed: #{e.message}")
|
|
62
|
+
# => "Could not generate a unique token for :code after 5 retries"
|
|
63
|
+
end
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Custom generator — proc as second argument
|
|
69
|
+
|
|
70
|
+
```ruby
|
|
71
|
+
class Order < ApplicationRecord
|
|
72
|
+
tokenize :reference, proc { |size| SecureRandom.alphanumeric(size) },
|
|
73
|
+
prefix: "ord", size: 12
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
Order.create!.reference # => "ord-aB3cD4eF5gH6"
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
The proc receives `size` and must return a String. The `prefix` is still
|
|
80
|
+
applied on top of whatever the proc returns.
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## Custom generator — method reference via `&`
|
|
85
|
+
|
|
86
|
+
```ruby
|
|
87
|
+
class Order < ApplicationRecord
|
|
88
|
+
def self.reference_generator(size)
|
|
89
|
+
SecureRandom.alphanumeric(size)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
tokenize :reference, &method(:reference_generator)
|
|
93
|
+
end
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
`method(:reference_generator)` converts the class method to a `Method`
|
|
97
|
+
object which responds to `call(size)`. This is identical to passing a proc.
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## Custom generator — inline block
|
|
102
|
+
|
|
103
|
+
```ruby
|
|
104
|
+
class Order < ApplicationRecord
|
|
105
|
+
# Parentheses required when passing a block to a method call inside a class
|
|
106
|
+
tokenize(:reference) { |size| SecureRandom.alphanumeric(size) }
|
|
107
|
+
end
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## Custom generator with no prefix — bypasses `has_secure_token`
|
|
113
|
+
|
|
114
|
+
Providing any generator always uses the callback path, even when no `prefix:`
|
|
115
|
+
is given and `has_secure_token` is available:
|
|
116
|
+
|
|
117
|
+
```ruby
|
|
118
|
+
class User < ApplicationRecord
|
|
119
|
+
tokenize :api_token, proc { |size| "usr_#{SecureRandom.hex(size / 2)}" }
|
|
120
|
+
end
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## Multiple tokenized attributes
|
|
126
|
+
|
|
127
|
+
```ruby
|
|
128
|
+
class ApiCredential < ApplicationRecord
|
|
129
|
+
tokenize :public_key, prefix: "pk", size: 32
|
|
130
|
+
tokenize :private_key, prefix: "sk", size: 64
|
|
131
|
+
end
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## Manual inclusion (non-Rails or non-AR classes)
|
|
137
|
+
|
|
138
|
+
```ruby
|
|
139
|
+
class MyModel
|
|
140
|
+
include TokenizeAttr::Concern
|
|
141
|
+
|
|
142
|
+
# Provide before_create and self.exists? for the concern to work.
|
|
143
|
+
end
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## Recommended migration
|
|
149
|
+
|
|
150
|
+
```ruby
|
|
151
|
+
# db/migrate/YYYYMMDDHHMMSS_add_token_to_access_tokens.rb
|
|
152
|
+
class AddTokenToAccessTokens < ActiveRecord::Migration[7.1]
|
|
153
|
+
def change
|
|
154
|
+
add_column :access_tokens, :token, :string
|
|
155
|
+
add_index :access_tokens, :token, unique: true
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## Checking which path `tokenize` will use
|
|
163
|
+
|
|
164
|
+
```ruby
|
|
165
|
+
# In a Rails console or script:
|
|
166
|
+
MyModel.respond_to?(:has_secure_token) # => true/false
|
|
167
|
+
# true → tokenize without prefix will use has_secure_token
|
|
168
|
+
# false → tokenize always uses the custom callback
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
## Preserving a pre-set token
|
|
174
|
+
|
|
175
|
+
The callback checks `present?` before generating, so a pre-set value is
|
|
176
|
+
always preserved:
|
|
177
|
+
|
|
178
|
+
```ruby
|
|
179
|
+
token = AccessToken.create!(token: "tok-my-custom-value")
|
|
180
|
+
token.token # => "tok-my-custom-value"
|
|
181
|
+
```
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
module TokenizeAttr
|
|
2
|
+
VERSION: String
|
|
3
|
+
|
|
4
|
+
class Error < StandardError
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
class RetryExceededError < Error
|
|
8
|
+
def initialize: (Symbol | String attribute, Integer retries) -> void
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# @api private
|
|
12
|
+
class Tokenizer
|
|
13
|
+
def self.apply: (
|
|
14
|
+
Class klass,
|
|
15
|
+
Symbol attribute,
|
|
16
|
+
generator: Proc?,
|
|
17
|
+
size: Integer,
|
|
18
|
+
prefix: String?,
|
|
19
|
+
retries: Integer
|
|
20
|
+
) -> void
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
module Concern
|
|
24
|
+
module ClassMethods
|
|
25
|
+
# generator may be passed as a positional Proc OR as a block.
|
|
26
|
+
def tokenize: (
|
|
27
|
+
Symbol attribute,
|
|
28
|
+
?Proc? generator,
|
|
29
|
+
?size: Integer,
|
|
30
|
+
?prefix: String?,
|
|
31
|
+
?retries: Integer
|
|
32
|
+
) ?{ (Integer) -> String } -> void
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: tokenize_attr
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Pawel Niemczyk
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: activesupport
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '7.0'
|
|
19
|
+
- - "<"
|
|
20
|
+
- !ruby/object:Gem::Version
|
|
21
|
+
version: '9'
|
|
22
|
+
type: :runtime
|
|
23
|
+
prerelease: false
|
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
25
|
+
requirements:
|
|
26
|
+
- - ">="
|
|
27
|
+
- !ruby/object:Gem::Version
|
|
28
|
+
version: '7.0'
|
|
29
|
+
- - "<"
|
|
30
|
+
- !ruby/object:Gem::Version
|
|
31
|
+
version: '9'
|
|
32
|
+
description: |
|
|
33
|
+
tokenize_attr adds a `tokenize` class macro to ActiveRecord models. When no
|
|
34
|
+
prefix is needed it transparently delegates to Rails' built-in
|
|
35
|
+
has_secure_token (Rails 5+). When a prefix is required it installs a
|
|
36
|
+
before_create callback backed by SecureRandom.base58 with configurable
|
|
37
|
+
size, prefix, and retry logic.
|
|
38
|
+
email:
|
|
39
|
+
- pniemczyk.info@gmail.com
|
|
40
|
+
executables: []
|
|
41
|
+
extensions: []
|
|
42
|
+
extra_rdoc_files: []
|
|
43
|
+
files:
|
|
44
|
+
- AGENTS.md
|
|
45
|
+
- CLAUDE.md
|
|
46
|
+
- LICENSE.txt
|
|
47
|
+
- README.md
|
|
48
|
+
- Rakefile
|
|
49
|
+
- lib/tokenize_attr.rb
|
|
50
|
+
- lib/tokenize_attr/concern.rb
|
|
51
|
+
- lib/tokenize_attr/errors.rb
|
|
52
|
+
- lib/tokenize_attr/tokenizer.rb
|
|
53
|
+
- lib/tokenize_attr/version.rb
|
|
54
|
+
- llms/overview.md
|
|
55
|
+
- llms/usage.md
|
|
56
|
+
- sig/tokenize_attr.rbs
|
|
57
|
+
homepage: https://github.com/pniemczyk/tokenize_attr
|
|
58
|
+
licenses:
|
|
59
|
+
- MIT
|
|
60
|
+
metadata:
|
|
61
|
+
homepage_uri: https://github.com/pniemczyk/tokenize_attr
|
|
62
|
+
source_code_uri: https://github.com/pniemczyk/tokenize_attr
|
|
63
|
+
rdoc_options: []
|
|
64
|
+
require_paths:
|
|
65
|
+
- lib
|
|
66
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
67
|
+
requirements:
|
|
68
|
+
- - ">="
|
|
69
|
+
- !ruby/object:Gem::Version
|
|
70
|
+
version: 3.2.0
|
|
71
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - ">="
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '0'
|
|
76
|
+
requirements: []
|
|
77
|
+
rubygems_version: 3.6.9
|
|
78
|
+
specification_version: 4
|
|
79
|
+
summary: Declarative secure token generation for ActiveRecord model attributes.
|
|
80
|
+
test_files: []
|