Skip to content

Commit 94b6cec

Browse files
committed
♻️ Refactor spec support harness for ROM
1 parent baa3a81 commit 94b6cec

File tree

8 files changed

+253
-136
lines changed

8 files changed

+253
-136
lines changed

.rubocop_gradual.lock

Lines changed: 14 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
2-
"README.md:2294530640": [
3-
[275, 3, 100, "Style/ClassMethodsDefinitions: Use `class << self` to define a class method.", 3592044714]
2+
"README.md:2478083419": [
3+
[277, 3, 100, "Style/ClassMethodsDefinitions: Use `class << self` to define a class method.", 3592044714]
44
],
55
"bin/bundle:247448467": [
66
[64, 5, 20, "ThreadSafety/ClassInstanceVariable: Avoid class instance variables.", 2485198147]
@@ -31,13 +31,12 @@
3131
[57, 13, 125, "Style/ClassMethodsDefinitions: Use `class << self` to define a class method.", 3604044426],
3232
[69, 13, 83, "Style/ClassMethodsDefinitions: Use `class << self` to define a class method.", 902334745]
3333
],
34-
"lib/omniauth/identity/models/rom.rb:4274893658": [
35-
[23, 9, 804, "Style/ClassMethodsDefinitions: Use `class << self` to define a class method.", 2641680277],
34+
"lib/omniauth/identity/models/rom.rb:1077115502": [
35+
[23, 9, 814, "Style/ClassMethodsDefinitions: Use `class << self` to define a class method.", 2641680277],
3636
[34, 33, 14, "ThreadSafety/ClassInstanceVariable: Avoid class instance variables.", 3392992668],
37-
[34, 48, 15, "ThreadSafety/ClassInstanceVariable: Avoid class instance variables.", 3094507655],
38-
[51, 13, 15, "ThreadSafety/ClassInstanceVariable: Avoid class instance variables.", 1768297560],
39-
[67, 18, 20, "ThreadSafety/ClassInstanceVariable: Avoid class instance variables.", 2230320645],
40-
[68, 54, 20, "ThreadSafety/ClassInstanceVariable: Avoid class instance variables.", 2230320645]
37+
[56, 13, 15, "ThreadSafety/ClassInstanceVariable: Avoid class instance variables.", 1768297560],
38+
[72, 18, 20, "ThreadSafety/ClassInstanceVariable: Avoid class instance variables.", 2230320645],
39+
[73, 54, 20, "ThreadSafety/ClassInstanceVariable: Avoid class instance variables.", 2230320645]
4140
],
4241
"lib/omniauth/identity/models/sequel.rb:101522047": [
4342
[44, 9, 3379, "Style/ClassMethodsDefinitions: Use `class << self` to define a class method.", 3595458005]
@@ -103,15 +102,13 @@
103102
[14, 1, 61, "RSpec/SpecFilePathFormat: Spec path should end with `omni_auth/identity/models/mongoid*_spec.rb`.", 2184534986],
104103
[32, 9, 71, "RSpec/SubjectStub: Do not stub methods of the object under test.", 399334056]
105104
],
106-
"spec_orms/rom_spec.rb:2718230340": [
107-
[42, 1, 57, "RSpec/SpecFilePathFormat: Spec path should end with `omni_auth/identity/models/rom*_spec.rb`.", 18469108],
108-
[43, 3, 254, "RSpec/LeakyConstantDeclaration: Stub constant instead of declaring explicitly.", 151871654],
109-
[51, 3, 12, "RSpec/BeforeAfterAll: Beware of using `before(:all)` as it may cause state to leak between tests. If you are using `rspec-rails`, and `use_transactional_fixtures` is enabled, then records created in `before(:all)` are not automatically rolled back.", 86334566]
105+
"spec_orms/rom_spec.rb:1364234352": [
106+
[5, 1, 57, "RSpec/SpecFilePathFormat: Spec path should end with `omni_auth/identity/models/rom*_spec.rb`.", 18469108],
107+
[6, 3, 12, "RSpec/BeforeAfterAll: Beware of using `before(:all)` as it may cause state to leak between tests. If you are using `rspec-rails`, and `use_transactional_fixtures` is enabled, then records created in `before(:all)` are not automatically rolled back.", 86334566]
110108
],
111-
"spec_orms/sequel_spec.rb:1275922107": [
112-
[22, 1, 60, "RSpec/SpecFilePathFormat: Spec path should end with `omni_auth/identity/models/sequel*_spec.rb`.", 36626431],
113-
[23, 3, 12, "RSpec/BeforeAfterAll: Beware of using `before(:all)` as it may cause state to leak between tests. If you are using `rspec-rails`, and `use_transactional_fixtures` is enabled, then records created in `before(:all)` are not automatically rolled back.", 86334566],
114-
[34, 7, 42, "Layout/EmptyLinesAfterModuleInclusion: Add an empty line after module inclusion.", 1846499429],
115-
[48, 9, 71, "RSpec/SubjectStub: Do not stub methods of the object under test.", 399334056]
109+
"spec_orms/sequel_spec.rb:1276231279": [
110+
[12, 1, 60, "RSpec/SpecFilePathFormat: Spec path should end with `omni_auth/identity/models/sequel*_spec.rb`.", 36626431],
111+
[13, 3, 12, "RSpec/BeforeAfterAll: Beware of using `before(:all)` as it may cause state to leak between tests. If you are using `rspec-rails`, and `use_transactional_fixtures` is enabled, then records created in `before(:all)` are not automatically rolled back.", 86334566],
112+
[39, 9, 71, "RSpec/SubjectStub: Do not stub methods of the object under test.", 399334056]
116113
]
117114
}

README.md

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -167,14 +167,16 @@ This gem is compatible with a wide range of Ruby versions and Ruby ORMs, as of M
167167
* mongoid 7.3, 7.4, 8.1, 9.0
168168
* bson 4.12, 4.15, 5.0, HEAD
169169
* sequel 5.86+
170-
* At least 5 different database ORM adapters, which connect to 15 different database clients!
170+
* rom-sql (Ruby Object Mapper) 3.7+
171+
* At least 6 different database ORM adapters, which connect to 15 different database clients!
171172

172173
| Databases | Adapter Libraries |
173-
| --------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
174+
|-----------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------|
174175
| MySQL, MariaDB, PostgreSQL, SQLite | [ActiveRecord](https://guides.rubyonrails.org/active_record_basics.html) |
175176
| CouchDB | [CouchPotato](https://github.com/langalex/couch_potato) |
176177
| MongoDB | [Mongoid](https://github.com/mongodb/mongoid) |
177178
| RethinkDB | [NoBrainer](http://nobrainer.io/) |
179+
| ADO, Amalgalite, IBM_DB, JDBC, MySQL, MariaDB, ODBC, Oracle, PostgreSQL, SQLAnywhere, SQLite, and TinyTDS | [rom-sql](https://rom-rb.org/) |
178180
| ADO, Amalgalite, IBM_DB, JDBC, MySQL, MariaDB, ODBC, Oracle, PostgreSQL, SQLAnywhere, SQLite, and TinyTDS | [Sequel](http://sequel.jeremyevans.net) |
179181

180182
## 🔧 Basic Usage
@@ -298,7 +300,23 @@ end
298300

299301
### Ruby Object Mapper
300302

301-
Would love to add a mixin for the [Ruby Object Mapper (ROM)](https://rom-rb.org/) if anyone wants to work on it!
303+
[ROM](https://rom-rb.org/) is an ORM for pretty much every database.
304+
305+
Include the `OmniAuth::Identity::Models::Rom` mixin and specify
306+
the `rom_container`, everything else is optional.
307+
308+
```ruby
309+
class Identity
310+
include OmniAuth::Identity::Models::Rom
311+
312+
# Configure the ROM container and relation
313+
rom_container -> { MyDatabase.rom } # See spec_orms/rom_spec.rb for example
314+
rom_relation_name :identities # optional, defaults to :idneitities
315+
owner_relation_name :owners # optional, for loading associated owner
316+
auth_key :email # optional, defaults to :email
317+
password_field :password_digest # optional, defaults to :password_digest
318+
end
319+
```
302320

303321
### Custom Auth Model
304322

lib/omniauth/identity/models/rom.rb

Lines changed: 67 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
require "bcrypt"
44

5+
require "rom"
6+
require "rom-sql"
7+
58
module OmniAuth
69
module Identity
710
module Models
@@ -12,26 +15,28 @@ module Models
1215
# class Identity
1316
# include OmniAuth::Identity::Models::Rom
1417
#
15-
# # Configure the ROM container and relation
16-
# self.rom_container = -> { MyDatabase.rom } # See spec_orms/rom_spec.rb for example
17-
# self.rom_relation_name = :identities
18-
# self.owner_relation_name = :owners # optional, for loading associated owner
19-
# self.auth_key_field = :email # optional, defaults to :email
20-
# self.password_field = :password_digest # optional, defaults to :password_digest
18+
# # Configure the ROM container and relation using the DSL (no `self.`):
19+
# rom_container -> { MyDatabase.rom } # accepts a proc or a container object
20+
# rom_relation_name :identities # optional, defaults to :identities
21+
# owner_relation_name :owners # optional, for loading associated owner
22+
# # Uses OmniAuth::Identity::Model.auth_key to set the auth key (defaults to :email)
23+
# auth_key :email
24+
# # Optional: override the password digest field name (defaults to :password_digest)
25+
# password_field :password_digest
2126
# end
2227
module Rom
2328
def self.included(base)
29+
# Align with other adapters: rely on OmniAuth::Identity::Model for API
2430
base.include(::OmniAuth::Identity::Model)
2531
base.extend(ClassMethods)
2632

2733
base.class_eval do
28-
# OmniAuth::Identity required method
34+
# OmniAuth::Identity required instance API
2935
# Authenticates the instance with the provided password
30-
#
31-
# @param password [String] The password to check
32-
# @return [self, false] Self if authenticated, false if not
36+
# Returns self on success, false otherwise (to match Model.authenticate contract)
3337
def authenticate(password)
34-
password_digest = @identity_data[self.class.password_field || :password_digest]
38+
digest_key = self.class.password_field
39+
password_digest = @identity_data[digest_key]
3540
return false unless password_digest
3641

3742
begin
@@ -44,45 +49,65 @@ def authenticate(password)
4449
end
4550

4651
module ClassMethods
47-
# @!attribute [r] DEFAULT_RELATION_NAME
48-
# Default relation name to use when rom_relation_name is not set
49-
# @return [Symbol]
52+
# Default ROM relation name when none is configured
5053
DEFAULT_RELATION_NAME = :identities
5154

52-
attr_accessor :rom_container, :rom_relation_name, :owner_relation_name, :auth_key_field, :password_field
55+
# Configuration DSL
56+
# These methods act like the DSL on `OmniAuth::Identity::Model` (e.g. `auth_key`) —
57+
# when called with an argument they set the configuration, and when called
58+
# without an argument they return the current value (with sensible defaults).
59+
def rom_container(value = false)
60+
@rom_container = value unless value == false
61+
container = @rom_container
62+
container.respond_to?(:call) ? container.call : container
63+
end
64+
65+
def rom_relation_name(value = false)
66+
@rom_relation_name = value unless value == false
67+
@rom_relation_name || DEFAULT_RELATION_NAME
68+
end
5369

54-
# OmniAuth::Identity required method
55-
def auth_key
56-
@auth_key_field || :email
70+
def owner_relation_name(value = false)
71+
@owner_relation_name = value unless value == false
72+
@owner_relation_name
5773
end
5874

59-
# OmniAuth::Identity required method
60-
# Locates a user based on the provided conditions
61-
#
62-
# @param conditions [Hash] The search conditions
63-
# @return [Object, nil] An instance of the identity model or nil if not found
75+
def password_field(value = false)
76+
@password_field = value unless value == false
77+
(@password_field || :password_digest).to_sym
78+
end
79+
80+
# Align with other adapters: use Model.auth_key (getter/setter) for the login attribute
81+
# Model.auth_key returns a String; convert to Symbol for ROM queries when needed.
82+
def auth_key_symbol
83+
(auth_key || "email").to_sym
84+
end
85+
86+
# Locate an identity given conditions (Hash) or a raw key value.
87+
# Mirrors other adapters by accepting a conditions hash.
88+
# Returns an instance or nil.
6489
def locate(conditions)
65-
key = conditions.is_a?(Hash) ? conditions[auth_key] || conditions[auth_key.to_s] : conditions
66-
container = rom_container.respond_to?(:call) ? rom_container.call : rom_container
67-
relation = container.relations[rom_relation_name || DEFAULT_RELATION_NAME]
68-
identity_data = relation.where(auth_key => key).one
69-
70-
if identity_data
71-
# Load the associated owner if configured
72-
if @owner_relation_name && identity_data[:owner_id]
73-
owner_relation = container.relations[@owner_relation_name]
74-
owner_data = owner_relation.where(id: identity_data[:owner_id]).one
75-
new(identity_data, owner_data)
76-
else
77-
new(identity_data)
78-
end
90+
key_value = if conditions.is_a?(Hash)
91+
conditions[auth_key_symbol] || conditions[auth_key.to_s]
92+
else
93+
conditions
94+
end
95+
return nil if key_value.nil?
96+
97+
relation = rom_container.relations[rom_relation_name]
98+
identity_data = relation.where(auth_key_symbol => key_value).one
99+
return nil unless identity_data
100+
101+
if owner_relation_name && identity_data[:owner_id]
102+
owner_data = rom_container.relations[owner_relation_name].where(id: identity_data[:owner_id]).one
103+
new(identity_data, owner_data)
79104
else
80-
nil
105+
new(identity_data)
81106
end
82107
end
83108
end
84109

85-
# Instance methods for OmniAuth::Identity compatibility
110+
# Instance shape matches other adapters for downstream usage
86111
attr_reader :uid, :email, :name, :info, :owner
87112

88113
def initialize(identity_data, owner_data = nil)
@@ -91,7 +116,7 @@ def initialize(identity_data, owner_data = nil)
91116
@uid = identity_data[:id]
92117
@email = identity_data[:email]
93118

94-
# Get name from owner if available, otherwise use email
119+
# Prefer owner name if available, else fall back to email
95120
@name = if owner_data && owner_data[:name]
96121
owner_data[:name]
97122
else
@@ -103,16 +128,15 @@ def initialize(identity_data, owner_data = nil)
103128
"name" => @name,
104129
}
105130

106-
# Expose owner for application use
107131
@owner = owner_data
108132
end
109133

110-
# Access to the underlying identity data hash
134+
# Hash-like access to underlying ROM tuple
111135
def [](key)
112136
@identity_data[key]
113137
end
114138

115-
# Access to owner data
139+
# Convenience accessor for owner id
116140
def owner_id
117141
@identity_data[:owner_id]
118142
end

rom_spec_refactored.txt

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
LOADING DEBUGGER: true
2+
3+
Randomized with seed 27610
4+
5+
OmniAuth::Identity::Models::Rom
6+
model
7+
::authenticate
8+
calls locate with additional scopes when provided
9+
recovers gracefully if locate is nil
10+
calls locate and then authenticate
11+
::locate
12+
returns nil if not found
13+
finds a record by auth key
14+
class definition
15+
does not raise an error
16+
owner_relation_name
17+
loads associated owner data when owner_relation_name is configured
18+
::authenticate
19+
returns false with incorrect password
20+
returns false for non-existent user
21+
authenticates with correct password
22+
23+
Finished in 1.89 seconds (files took 0.98284 seconds to load)
24+
10 examples, 0 failures
25+
26+
Randomized with seed 27610
27+

0 commit comments

Comments
 (0)