familia 2.0.0.pre22 → 2.0.0.pre23

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: cf37d5963fa9ae33323b70ad4eea48cfa45e91aa59ea46ecce2ac85644a9d0e2
4
- data.tar.gz: 8880f7e2914b12db9a0cdcf2f741968c7c43731ef15833a921be72df19de70cd
3
+ metadata.gz: cc6c562adc770554d50a5d8d97be46b8a924d04ed6a28e7447a765990e69d89b
4
+ data.tar.gz: 97812dfbbcd8b7cb2e3c9447558e7b1b6eb30181d9eed8c3ae86e0371f83cf7e
5
5
  SHA512:
6
- metadata.gz: 31b585405895213ee9857a66306ee2e7b60ce2f26568915571bc8c992e1169ff8589856c40253e2b8ef8081fd3805b0f32f0a769d85108a11528786621ac4440
7
- data.tar.gz: 715766983ba9c44603bc8a94c240dc716130cc70cc182fd84dec1cc5b17333de32e52b1677610d62b6aba047aadada2cdf65b61867df1d3bffab08f94d8c4067
6
+ metadata.gz: 6d41492ba9f38dafacfe65a37a792c6f50ad90e406718131b0e226a6003b27008a1057851eb1d73ed1ff8baf8cf95312e684b290e23b3bbc2c9b5d453f0ba2ea
7
+ data.tar.gz: 2bf7cb92e0e06b623429dd0ff3b1da46686236329f5f3540e9f344195d1a51b50a209fc3b7d6538827c91e51194dd1014f9e5efd536a264c08283c505ea5044f
@@ -7,11 +7,11 @@ on:
7
7
 
8
8
  jobs:
9
9
  claude-review:
10
- # Optional: Filter by PR author
11
- # if: |
12
- # github.event.pull_request.user.login == 'external-contributor' ||
13
- # github.event.pull_request.user.login == 'new-developer' ||
14
- # github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR'
10
+ # Run on opened/synchronize, but only on 'labeled' if label is 'claude-review'
11
+ if: |
12
+ github.event.action == 'opened' ||
13
+ github.event.action == 'synchronize' ||
14
+ (github.event.action == 'labeled' && github.event.label.name == 'claude-review')
15
15
 
16
16
  runs-on: ubuntu-latest
17
17
  permissions:
@@ -50,3 +50,6 @@ jobs:
50
50
  # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
51
51
  # or https://docs.anthropic.com/en/docs/claude-code/sdk#command-line for available options
52
52
  allowed_tools: "Bash(gh issue view:*),Bash(gh search:*),Bash(gh issue list:*),Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh pr list:*)"
53
+
54
+ # Allow bot-initiated workflows (e.g., qodo, dependabot)
55
+ allowed_bots: "*"
data/CHANGELOG.rst CHANGED
@@ -7,6 +7,39 @@ The format is based on `Keep a Changelog <https://keepachangelog.com/en/1.1.0/>`
7
7
 
8
8
  <!--scriv-insert-here-->
9
9
 
10
+ .. _changelog-2.0.0.pre23:
11
+
12
+ 2.0.0.pre23 — 2025-12-22
13
+ ========================
14
+
15
+ Added
16
+ -----
17
+
18
+ - Add ``:through`` option to ``participates_in`` for join model support.
19
+ Enables storing additional attributes (role, permissions, metadata) on
20
+ participation relationships via an intermediate model. The through model
21
+ uses deterministic keys and supports idempotent operations - adding an
22
+ existing participant updates rather than duplicates.
23
+
24
+ Security
25
+ --------
26
+
27
+ - Add validation for through model attributes to prevent arbitrary method
28
+ invocation. Only fields defined on the through model schema can be set
29
+ via the ``through_attrs`` parameter.
30
+
31
+ Documentation
32
+ -------------
33
+
34
+ - Add YARD documentation for the ``:through`` parameter on both
35
+ ``participates_in`` and ``class_participates_in`` methods.
36
+
37
+ AI Assistance
38
+ -------------
39
+
40
+ - Implementation design and code review assistance provided by Claude.
41
+ Security hardening for attribute validation added based on Qodo review.
42
+
10
43
  .. _changelog-2.0.0.pre22:
11
44
 
12
45
  2.0.0.pre22 — 2025-12-03
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- familia (2.0.0.pre22)
4
+ familia (2.0.0.pre23)
5
5
  benchmark (~> 0.4)
6
6
  concurrent-ruby (~> 1.3)
7
7
  connection_pool (~> 2.5)
@@ -17,10 +17,10 @@ GEM
17
17
  specs:
18
18
  ast (2.4.3)
19
19
  base64 (0.3.0)
20
- benchmark (0.4.1)
21
- bigdecimal (3.2.3)
22
- concurrent-ruby (1.3.5)
23
- connection_pool (2.5.3)
20
+ benchmark (0.5.0)
21
+ bigdecimal (3.3.1)
22
+ concurrent-ruby (1.3.6)
23
+ connection_pool (2.5.5)
24
24
  csv (3.3.5)
25
25
  date (3.5.0)
26
26
  debug (1.11.0)
@@ -69,7 +69,7 @@ GEM
69
69
  lint_roller (1.1.0)
70
70
  logger (1.7.0)
71
71
  minitest (5.26.0)
72
- oj (3.16.11)
72
+ oj (3.16.13)
73
73
  bigdecimal (>= 3.0)
74
74
  ostruct (>= 0.2)
75
75
  ostruct (0.6.3)
@@ -99,7 +99,7 @@ GEM
99
99
  redcarpet (3.6.1)
100
100
  redis (5.4.1)
101
101
  redis-client (>= 0.22.0)
102
- redis-client (0.25.1)
102
+ redis-client (0.26.2)
103
103
  connection_pool
104
104
  reek (6.5.0)
105
105
  dry-schema (~> 1.13)
@@ -154,7 +154,7 @@ GEM
154
154
  base64
155
155
  ruby-progressbar (1.13.0)
156
156
  stackprof (0.2.27)
157
- stringio (3.1.7)
157
+ stringio (3.1.9)
158
158
  timecop (0.9.10)
159
159
  tryouts (3.7.1)
160
160
  concurrent-ruby (~> 1.0, < 2)
@@ -1,15 +1,23 @@
1
- The Naming Problem
1
+ # Bidirectional Relationships in Familia
2
2
 
3
- bidirectional: true is misleading because:
3
+ > **Document Status**: Updated 2025-12 to reflect current implementation including
4
+ > `:through` option and `config_name`-based method naming.
5
+
6
+ ## The Original Naming Problem (Now Resolved)
7
+
8
+ The parameter was renamed from `bidirectional:` to `generate_participant_methods:`.
9
+
10
+ `bidirectional: true` was misleading because:
4
11
 
5
12
  1. It's not truly bidirectional - It only helps you manage membership in specific instances, not query all memberships
6
- 2. Better name would be: generate_participant_methods: true
13
+ 2. Better name: `generate_participant_methods: true` (**now implemented**)
7
14
  3. True bidirectionality would mean both sides can easily query their relationships
8
15
 
9
- What True Bidirectionality Should Look Like
16
+ ## What True Bidirectionality Should Look Like
10
17
 
11
- Option 1: Auto-generate reverse collections
18
+ ### Option 1: Auto-generate reverse collections
12
19
 
20
+ ```ruby
13
21
  class Customer < Familia::Horreum
14
22
  participates_in Team, :members, bidirectional: true, reverse: :teams
15
23
  end
@@ -18,9 +26,11 @@ end
18
26
  customer.teams # All teams this customer is in
19
27
  customer.teams.count # How many teams
20
28
  customer.teams.include?(team_id) # Check membership
29
+ ```
21
30
 
22
- Option 2: Make bidirectional actually bidirectional
31
+ ### Option 2: Make bidirectional actually bidirectional
23
32
 
33
+ ```ruby
24
34
  class Customer < Familia::Horreum
25
35
  participates_in Team, :members, bidirectional: true
26
36
  end
@@ -28,10 +38,13 @@ end
28
38
  # Should auto-generate (using pluralized class name):
29
39
  customer.teams # Since we're participating in Team class
30
40
  customer.organizations # If also participating in Organization class
41
+ ```
31
42
 
32
- Current Implementation Gap
43
+ ## The Implementation Gap (Historical Context)
33
44
 
34
- Looking at the actual usage pattern:
45
+ Looking at the original usage pattern:
46
+
47
+ ```ruby
35
48
  # Easy to go from Team → Customers
36
49
  team.members.to_a # Simple!
37
50
  customers = Customer.multiget(*team.members.to_a)
@@ -41,89 +54,219 @@ customer.participations.members
41
54
  .select { |k| k.start_with?("team:") }
42
55
  .map { |k| k.split(':')[1] }
43
56
  # ... etc - complicated!
57
+ ```
58
+
59
+ ### What Was Really Happening
44
60
 
45
- What's Really Happening
61
+ The bidirectional flag only controlled whether these instance-to-instance methods were generated:
62
+ - `customer.add_to_team_members(specific_team)`
63
+ - `customer.in_team_members?(specific_team)`
46
64
 
47
- The bidirectional flag only controls whether these instance-to-instance methods are generated:
48
- - customer.add_to_team_members(specific_team)
49
- - customer.in_team_members?(specific_team)
65
+ It did NOT create instance-to-collection methods:
66
+ - `customer.teams` ❌
67
+ - `customer.all_team_memberships` ❌
50
68
 
51
- It does NOT create instance-to-collection methods:
52
- - customer.teams ❌
53
- - customer.all_team_memberships ❌
69
+ ---
54
70
 
71
+ ## Current Implementation (Familia 2.x)
55
72
 
56
- The functionality we are hoping to achieve:
57
- Bidirectional Relationships Feature Spec
73
+ The solution now auto-generates reverse collection methods on participant classes.
58
74
 
59
- Problem
75
+ ### Implemented API (Using `_instance` Suffix + `config_name` Naming)
60
76
 
61
- Currently, Familia relationships are asymmetric. While you can easily query team.members to get all members, there's no convenient way to get
62
- all teams a user belongs to without manually parsing the participations reverse index.
77
+ **Important**: Method names are based on the target class's `config_name` (snake_case of
78
+ the full class name), not just the class basename. This ensures uniqueness when
79
+ multiple classes have the same basename (e.g., `Admin::Team` vs `Public::Team`).
63
80
 
64
- Solution
81
+ ```ruby
82
+ class Domain < Familia::Horreum
83
+ # Customer.config_name => "customer"
84
+ participates_in Customer, :domains
85
+ # Auto-generates on Domain:
86
+ # domain.customer_instances
87
+ # domain.customer_ids
88
+ # domain.customer?
89
+ # domain.customer_count
65
90
 
66
- Auto-generate reverse collection methods on participant classes to provide symmetric access to relationships.
91
+ # With custom naming via `as:`
92
+ participates_in Customer, :partner_domains, as: :partners
93
+ # Auto-generates:
94
+ # domain.partners_instances
95
+ # domain.partners_ids
96
+ # domain.partners?
97
+ # domain.partners_count
98
+ end
67
99
 
68
- ## Implemented API (Using _instance Suffix Pattern)
100
+ class ProjectTeam < Familia::Horreum
101
+ # Note: config_name is "project_team", not "team"
102
+ end
69
103
 
70
104
  class User < Familia::Horreum
71
- participates_in Team, :members # Auto-generates: user.team_instances
72
- participates_in Team, :admins # Also adds to: user.team_instances (union)
73
- participates_in Organization, :employees # Auto-generates: user.organization_instances
74
- participates_in Organization, :contractors, as: :contracting_orgs # Custom name
105
+ participates_in ProjectTeam, :members
106
+ # Auto-generates: user.project_team_instances (NOT user.team_instances)
107
+ end
108
+ ```
109
+
110
+ ### Forward Direction (Target Class Methods)
111
+
112
+ ```ruby
113
+ customer.domains # → SortedSet of domain IDs
114
+ customer.add_domains_instance(domain) # → Adds + tracks participation
115
+ customer.remove_domains_instance(domain) # → Removes + untracks
116
+ ```
117
+
118
+ ### Reverse Direction (Participant Class Methods)
119
+
120
+ ```ruby
121
+ domain.customer_instances # → Array of Customer instances
122
+ domain.customer_ids # → Array of customer IDs (no object loading)
123
+ domain.customer? # → Boolean: participates in any customers?
124
+ domain.customer_count # → Integer count without loading objects
125
+ ```
126
+
127
+ ### Implementation Status
128
+
129
+ | Method Pattern | Forward (Target) | Reverse (Participant) |
130
+ |----------------|------------------|----------------------|
131
+ | `*_instances` | `add_*_instance`, `remove_*_instance` | `{config_name}_instances` |
132
+ | `*_ids` | N/A (use collection directly) | `{config_name}_ids` |
133
+ | `*?` | N/A | `{config_name}?` |
134
+ | `*_count` | N/A (use `collection.size`) | `{config_name}_count` |
135
+
136
+ ---
137
+
138
+ ## Through Models (New in 2.x)
139
+
140
+ The `:through` option enables rich join model patterns for storing additional
141
+ membership data (roles, timestamps, status).
142
+
143
+ ```ruby
144
+ class OrganizationMembership < Familia::Horreum
145
+ feature :object_identifier # Required for through models
146
+ prefix :org_membership
147
+
148
+ field :organization_id
149
+ field :customer_id
150
+ field :role # 'owner', 'admin', 'member'
151
+ field :status # 'active', 'pending', 'declined'
152
+ field :invited_at
153
+ field :joined_at
154
+ end
155
+
156
+ class Customer < Familia::Horreum
157
+ participates_in Organization, :members,
158
+ score: :joined,
159
+ through: :OrganizationMembership
75
160
  end
161
+ ```
162
+
163
+ ### Through Model Behavior
164
+
165
+ ```ruby
166
+ # Adding creates/updates the through model
167
+ membership = org.add_members_instance(customer, through_attrs: { role: 'admin' })
168
+ membership.role # => 'admin'
169
+
170
+ # Chaining pattern also works
171
+ membership = org.add_members_instance(customer)
172
+ membership.role = 'admin'
173
+ membership.save
76
174
 
77
- # Forward (existing)
78
- team.members # SortedSet of user IDs
79
- team.add_members_instance(user) # → Adds to collection + tracks participation
80
- team.remove_members_instance(user) # → Removes from collection + untracks
175
+ # Removing destroys the through model
176
+ org.remove_members_instance(customer) # OrganizationMembership is deleted
177
+ ```
81
178
 
82
- # Reverse (NEW)
83
- user.team_instances # → Array of Team instances user belongs to
84
- user.team_ids # → Array of team IDs (efficient, no loading)
85
- user.team? # → Boolean: belongs to any teams?
86
- user.team_count # → Count without loading objects
179
+ ### Through Model Requirements
87
180
 
88
- # Custom naming (user chooses base name via as: parameter)
89
- user.contracting_orgs_instances # Array of Organization instances
90
- user.contracting_orgs_ids # Array of IDs
91
- user.contracting_orgs? # → Boolean
92
- user.contracting_orgs_count # → Count
181
+ - Must have `feature :object_identifier` enabled
182
+ - Through model is auto-created on add, auto-destroyed on remove
183
+ - Attributes can be passed inline via `through_attrs:` or set after
184
+
185
+ ---
93
186
 
94
187
  ## Naming Rationale: Why `_instance` Suffix?
95
188
 
96
- The implementation uses an `_instance` suffix pattern instead of pluralization/singularization to avoid fragility:
189
+ The implementation uses an `_instance` suffix pattern instead of pluralization/singularization:
97
190
 
98
191
  **Target Methods (Forward Direction):**
99
- - `team.add_members_instance(user)` instead of `team.add_member(user)`
100
- - `team.remove_members_instance(user)` instead of `team.remove_member(user)`
192
+ - `customer.add_domains_instance(domain)` instead of `customer.add_domain(domain)`
193
+ - `customer.remove_domains_instance(domain)` instead of `customer.remove_domain(domain)`
101
194
 
102
195
  **Reverse Collection Methods:**
103
- - `user.team_instances` instead of `user.teams`
196
+ - `domain.customer_instances` instead of `domain.customers`
104
197
  - `user.organization_instances` instead of `user.organizations`
105
198
 
106
199
  **Benefits:**
107
- 1. **No irregular plurals** - Avoids issues with words like "person/people", "child/children", "foot/feet"
200
+ 1. **No irregular plurals** - Avoids issues with "person/people", "child/children", "foot/feet"
108
201
  2. **Clear intent** - The suffix makes it obvious you're working with instances, not counts or IDs
109
202
  3. **Consistent pattern** - Same suffix for both forward and reverse operations
110
- 4. **No external dependencies** - Removes need for inflection libraries like `dry-inflector`
203
+ 4. **No external dependencies** - No need for inflection libraries like `dry-inflector`
111
204
  5. **Predictable** - Easy to remember and document
112
205
 
113
206
  **Trade-off:**
114
207
  - Slightly more verbose, but eliminates an entire class of edge case bugs
115
208
 
116
- Key Requirements
209
+ ---
210
+
211
+ ## The `method_prefix:` Option
212
+
213
+ The default implementation uses `config_name` which can be verbose for namespaced
214
+ classes (e.g., `admin_project_team_instances`). The `method_prefix:` option provides
215
+ explicit control over reverse method naming:
216
+
217
+ ```ruby
218
+ participates_in Admin::ProjectTeam, :members, method_prefix: :team
219
+ # Generates: user.team_instances instead of user.admin_project_team_instances
220
+ ```
221
+
222
+ ### Parameter Priority
223
+
224
+ When multiple naming options are provided, the most specific wins:
225
+
226
+ | Parameter | Scope | Priority |
227
+ |-----------|-------|----------|
228
+ | `as:` | Single collection only | 1 (highest) |
229
+ | `method_prefix:` | All collections for target class | 2 |
230
+ | (default) | Uses `config_name` | 3 (lowest) |
231
+
232
+ ### Examples
233
+
234
+ ```ruby
235
+ # Default: uses config_name
236
+ participates_in Admin::ProjectTeam, :members
237
+ # → user.admin_project_team_instances, user.admin_project_team_ids, etc.
238
+
239
+ # With method_prefix: shorter, cleaner names
240
+ participates_in Admin::ProjectTeam, :members, method_prefix: :team
241
+ # → user.team_instances, user.team_ids, user.team?, user.team_count
242
+
243
+ # With as: overrides for specific collection only
244
+ participates_in Admin::ProjectTeam, :members, as: :my_teams
245
+ # → user.my_teams_instances, user.my_teams_ids, etc.
246
+
247
+ # Both as: and method_prefix: - as: wins (more specific)
248
+ participates_in Admin::ProjectTeam, :members, method_prefix: :team, as: :my_teams
249
+ # → user.my_teams_instances (as: takes precedence)
250
+ ```
251
+
252
+ This gives developers explicit control over method naming while maintaining
253
+ the predictability of the default system.
254
+
255
+ ---
256
+
257
+ ## Key Requirements (All Implemented)
117
258
 
118
- 1. Automatic generation - No manual method definitions needed
119
- 2. Multiple collections - Union of all collections to same target class
120
- 3. Performance - Efficient ID-only access without loading objects (no caching for data freshness)
121
- 4. Custom naming - Override auto-generated names when needed via `as:` parameter
122
- 5. Thread-safe - No caching means no stale data or cache invalidation complexity
259
+ 1. **Automatic generation** - No manual method definitions needed
260
+ 2. **Multiple collections** - Union of all collections to same target class
261
+ 3. **Performance** - Efficient ID-only access without loading objects
262
+ 4. **Custom naming** - Override auto-generated names via `as:` or `method_prefix:` parameters ✓
263
+ 5. **Thread-safe** - No caching means no stale data or cache invalidation complexity
264
+ 6. **Through models** - Rich join data via `:through` option ✓
123
265
 
124
- Benefits
266
+ ## Benefits
125
267
 
126
- - Symmetry - Both directions equally convenient
127
- - Discoverability - Natural Ruby method names
128
- - Efficiency - Choose between full objects, IDs, or counts
129
- - Backwards compatible - All existing code continues to work
268
+ - **Symmetry** - Both directions equally convenient
269
+ - **Discoverability** - Natural Ruby method names
270
+ - **Efficiency** - Choose between full objects, IDs, or counts
271
+ - **Backwards compatible** - All existing code continues to work
272
+ - **Extensible** - Through models enable rich membership data