type_scopes 0.4.0 → 0.6.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +17 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +36 -0
- data/README.md +32 -18
- data/Rakefile +8 -0
- data/lib/type_scopes/boolean.rb +15 -0
- data/lib/type_scopes/numeric.rb +17 -0
- data/lib/type_scopes/string.rb +40 -0
- data/lib/type_scopes/time.rb +23 -0
- data/lib/type_scopes/version.rb +2 -2
- data/lib/type_scopes.rb +32 -11
- data/test/test_helper.rb +44 -0
- data/test/type_scopes/boolean_test.rb +29 -0
- data/test/type_scopes/numeric_test.rb +49 -0
- data/test/type_scopes/string_test.rb +90 -0
- data/test/type_scopes/time_test.rb +49 -0
- data/type_scopes.gemspec +2 -2
- metadata +25 -10
- data/lib/boolean_scopes.rb +0 -29
- data/lib/numeric_scopes.rb +0 -30
- data/lib/string_scopes.rb +0 -31
- data/lib/timestamp_scopes.rb +0 -35
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 47e7400e39752e376e84d65854ac9eb4130505a6ee311aad99c7d5c132dceb02
|
4
|
+
data.tar.gz: 7a6d6e7d9dbe5d0c07771f175084c0727d3733fcf3ddafa3cd20b8cc50f3b94a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8f8d0cb2bf88623c23ac42f3bfc763fbbc2dfa75a741a7a0e92ffedc479cb0e5181de44397047c85719eef0f4bc831835d06cbf7f048b2f8bcc99200c5ffabb0
|
7
|
+
data.tar.gz: 5ec206a432eaa902c84d2b7ad535e8422ca2394eebd32fd04c17005ba6fb71b9e4097806a112be602ac1408124904293f939bca43370daa2d0ca5d8d60c0ce47
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
# Changelog of Type scopes
|
2
|
+
|
3
|
+
## 0.6.1 (2024-02-22)
|
4
|
+
|
5
|
+
- Check if table exists to prevent from ActiveRecord::StatementInvalid
|
6
|
+
|
7
|
+
## 0.6 (2021-10-15)
|
8
|
+
|
9
|
+
- Refactor by switching modules into classes.
|
10
|
+
|
11
|
+
New usage is `TypeScopes.inject` instead of `include` :
|
12
|
+
|
13
|
+
```ruby
|
14
|
+
class Model < ApplicationRecord
|
15
|
+
TypeScopes.inject(self)
|
16
|
+
end
|
17
|
+
```
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
GEM
|
2
|
+
remote: https://rubygems.org/
|
3
|
+
specs:
|
4
|
+
activemodel (7.0.4.1)
|
5
|
+
activesupport (= 7.0.4.1)
|
6
|
+
activerecord (7.0.4.1)
|
7
|
+
activemodel (= 7.0.4.1)
|
8
|
+
activesupport (= 7.0.4.1)
|
9
|
+
activesupport (7.0.4.1)
|
10
|
+
concurrent-ruby (~> 1.0, >= 1.0.2)
|
11
|
+
i18n (>= 1.6, < 2)
|
12
|
+
minitest (>= 5.1)
|
13
|
+
tzinfo (~> 2.0)
|
14
|
+
concurrent-ruby (1.1.10)
|
15
|
+
i18n (1.12.0)
|
16
|
+
concurrent-ruby (~> 1.0)
|
17
|
+
mini_portile2 (2.8.1)
|
18
|
+
minitest (5.17.0)
|
19
|
+
pg (1.4.5)
|
20
|
+
rake (13.0.6)
|
21
|
+
sqlite3 (1.6.0)
|
22
|
+
mini_portile2 (~> 2.8.0)
|
23
|
+
tzinfo (2.0.5)
|
24
|
+
concurrent-ruby (~> 1.0)
|
25
|
+
|
26
|
+
PLATFORMS
|
27
|
+
ruby
|
28
|
+
|
29
|
+
DEPENDENCIES
|
30
|
+
activerecord
|
31
|
+
pg
|
32
|
+
rake
|
33
|
+
sqlite3
|
34
|
+
|
35
|
+
BUNDLED WITH
|
36
|
+
2.2.22
|
data/README.md
CHANGED
@@ -1,8 +1,13 @@
|
|
1
1
|
# Type Scopes
|
2
2
|
|
3
|
-
Type scopes creates useful scopes based on the type of the columns of your models.
|
4
|
-
|
3
|
+
Type scopes creates useful semantic scopes based on the type of the columns of your models.
|
4
|
+
The goal is help to write eloquent code such as:
|
5
5
|
|
6
|
+
```ruby
|
7
|
+
Transaction.paid_after(Date.yesterday).amount_between(100, 200).not_refunded
|
8
|
+
```
|
9
|
+
|
10
|
+
It handles dates, times, strings, numerics and booleans.
|
6
11
|
Here are examples for all the available scopes:
|
7
12
|
|
8
13
|
```ruby
|
@@ -10,7 +15,7 @@ Here are examples for all the available scopes:
|
|
10
15
|
# amount: decimal
|
11
16
|
# description: string
|
12
17
|
class Transaction < ActiveRecord::Base
|
13
|
-
|
18
|
+
TypeScopes.inject self
|
14
19
|
end
|
15
20
|
|
16
21
|
# Time scopes
|
@@ -35,8 +40,21 @@ Transaction.amount_not_within(100, 200) # => where("amount <= 100 OR amount >= 2
|
|
35
40
|
|
36
41
|
# String scopes
|
37
42
|
Transaction.description_contains("foo") # => where("description LIKE '%foo%'")
|
43
|
+
Transaction.description_contains("foo", sensitive: false) # => where("description ILIKE '%foo%'")
|
38
44
|
Transaction.description_starts_with("foo") # => where("description LIKE 'foo%'")
|
45
|
+
Transaction.description_starts_with("foo", sensitive: false) # => where("description ILIKE 'foo%'")
|
46
|
+
Transaction.description_does_not_start_with("foo") # => where("description NOT LIKE 'foo%'")
|
47
|
+
Transaction.description_does_not_start_with("foo", sensitive: false) # => where("description NOT ILIKE 'foo%'")
|
39
48
|
Transaction.description_ends_with("foo") # => where("description LIKE '%foo'")
|
49
|
+
Transaction.description_ends_with("foo", sensitive: false) # => where("description ILIKE '%foo'")
|
50
|
+
Transaction.description_does_not_end_with("foo") # => where("description NOT LIKE '%foo'")
|
51
|
+
Transaction.description_does_not_end_with("foo", sensitive: false) # => where("description NOT ILIKE '%foo'")
|
52
|
+
Transaction.description_like("%foo%") # => where("description LIKE '%foo%'")
|
53
|
+
Transaction.description_not_like("%foo%") # => where("description NOT LIKE '%foo%'")
|
54
|
+
Transaction.description_ilike("%foo%") # => where("description ILIKE '%foo%'")
|
55
|
+
Transaction.description_not_ilike("%foo%") # => where("description NOT ILIKE '%foo%'")
|
56
|
+
Transaction.description_matches("^Regex$") # => where("description ~ '^Regex$'")
|
57
|
+
Transaction.description_does_not_match("^Regex$") # => where("description !~ '^Regex$'")
|
40
58
|
|
41
59
|
# Boolean scopes
|
42
60
|
Transaction.non_profit # => where("non_profit = true")
|
@@ -49,7 +67,7 @@ Transaction.was_processed # => where("was_processed = true")
|
|
49
67
|
Transaction.was_not_processed # => where("was_processed = false")
|
50
68
|
```
|
51
69
|
|
52
|
-
For the string
|
70
|
+
For the string colums, the pattern matching is escaped. So it's safe to provide directly a user input. There is an exception for the `column_like`, `column_ilike`, `column_matches` and `column_does_not_match` where the pattern is not escaped and you shouldn't provide untrusted strings.
|
53
71
|
|
54
72
|
```ruby
|
55
73
|
Transaction.description_contains("%foo_") # => where("description LIKE '%[%]foo[_]%'")
|
@@ -57,26 +75,22 @@ Transaction.description_contains("%foo_") # => where("description LIKE '%[%]foo[
|
|
57
75
|
|
58
76
|
## Install
|
59
77
|
|
60
|
-
Add to your Gemfile
|
78
|
+
Add to your Gemfile `gem "type_scopes"` and run in your terminal `bundle install`.
|
79
|
+
Then call `TypeScopes.inject self` from your models:
|
61
80
|
|
62
81
|
```ruby
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
82
|
+
# /app/models/transaction.rb
|
83
|
+
class Transaction < ApplicationRecord
|
84
|
+
# Creates scope for all supported column types
|
85
|
+
TypeScopes.inject self
|
67
86
|
|
68
|
-
|
69
|
-
|
70
|
-
```
|
71
|
-
|
72
|
-
Then include TypeScopes from your models:
|
73
|
-
|
74
|
-
```ruby
|
75
|
-
class Transaction < ActiveRecord::Base
|
76
|
-
include TypeScopes
|
87
|
+
# Or if you prefer to enable scopes for specific columns only
|
88
|
+
TypeScopes.inject self, :amount, :paid_at
|
77
89
|
end
|
78
90
|
```
|
79
91
|
|
92
|
+
In case there is a conflict with a scope name, TypeScopes won't over write your existing scope. You can safely inject TypeScopes and it won't break any scope defined previously.
|
93
|
+
|
80
94
|
## MIT License
|
81
95
|
|
82
96
|
Made by [Base Secrète](https://basesecrete.com/en).
|
data/Rakefile
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
class TypeScopes::Boolean < TypeScopes
|
2
|
+
def self.types
|
3
|
+
["bool", "boolean", "tinyint(1)"].freeze
|
4
|
+
end
|
5
|
+
|
6
|
+
def self.inject_for_column(model, name)
|
7
|
+
append_scope(model, :"#{name}", lambda { where(name => true) })
|
8
|
+
prefix, suffix = /\A(has|is|was)_(.+)\z/.match(name).to_a[1..2]
|
9
|
+
if prefix && suffix
|
10
|
+
append_scope(model, :"#{prefix}_not_#{suffix}", lambda { where(name => false) })
|
11
|
+
else
|
12
|
+
append_scope(model, :"not_#{name}", lambda { where(name => false) })
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
class TypeScopes::Numeric < TypeScopes
|
2
|
+
def self.types
|
3
|
+
["integer", "double precision", "numeric", "bigint", "decimal"].freeze
|
4
|
+
end
|
5
|
+
|
6
|
+
def self.inject_for_column(model, name)
|
7
|
+
column = model.arel_table[name]
|
8
|
+
append_scope(model, :"#{name}_to", lambda { |value| where(column.lteq(value)) })
|
9
|
+
append_scope(model, :"#{name}_from", lambda { |value| where(column.gteq(value)) })
|
10
|
+
append_scope(model, :"#{name}_above", lambda { |value| where(column.gt(value)) })
|
11
|
+
append_scope(model, :"#{name}_below", lambda { |value| where(column.lt(value)) })
|
12
|
+
append_scope(model, :"#{name}_between", lambda { |from, to| where(name => from..to) })
|
13
|
+
append_scope(model, :"#{name}_not_between", lambda { |from, to| where.not(name => from..to) })
|
14
|
+
append_scope(model, :"#{name}_within", lambda { |from, to| where(column.gt(from)).where(column.lt(to)) })
|
15
|
+
append_scope(model, :"#{name}_not_within", lambda { |from, to| where(column.lteq(from).or(column.gteq(to))) })
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
class TypeScopes::String < TypeScopes
|
2
|
+
def self.types
|
3
|
+
["character", "text", "varchar"].freeze
|
4
|
+
end
|
5
|
+
|
6
|
+
def self.inject_for_column(model, name)
|
7
|
+
column = model.arel_table[name]
|
8
|
+
append_scope(model, :"#{name}_like", lambda { |str, sensitive: true| where(column.matches(str, nil, sensitive)) })
|
9
|
+
append_scope(model, :"#{name}_not_like", lambda { |str, sensitive: true| where(column.does_not_match(str, nil, sensitive)) })
|
10
|
+
append_scope(model, :"#{name}_ilike", lambda { |str| where(column.matches(str)) })
|
11
|
+
append_scope(model, :"#{name}_not_ilike", lambda { |str| where(column.does_not_match(str)) })
|
12
|
+
|
13
|
+
append_scope(model, :"#{name}_contains", lambda { |str, sensitive: true|
|
14
|
+
send("#{name}_like", "%#{sanitize_sql_like(str)}%", sensitive: sensitive)
|
15
|
+
})
|
16
|
+
|
17
|
+
append_scope(model, :"#{name}_does_not_contain", lambda { |str, sensitive: true|
|
18
|
+
send("#{name}_not_like", "%#{sanitize_sql_like(str)}%", sensitive: sensitive)
|
19
|
+
})
|
20
|
+
|
21
|
+
append_scope(model, :"#{name}_starts_with", lambda { |str, sensitive: true|
|
22
|
+
send("#{name}_like", "#{sanitize_sql_like(str)}%", sensitive: sensitive)
|
23
|
+
})
|
24
|
+
|
25
|
+
append_scope(model, :"#{name}_does_not_start_with", lambda { |str, sensitive: true|
|
26
|
+
send("#{name}_not_like", "#{sanitize_sql_like(str)}%", sensitive: sensitive)
|
27
|
+
})
|
28
|
+
|
29
|
+
append_scope(model, :"#{name}_ends_with", lambda { |str, sensitive: true|
|
30
|
+
send("#{name}_like", "%#{sanitize_sql_like(str)}", sensitive: sensitive)
|
31
|
+
})
|
32
|
+
|
33
|
+
append_scope(model, :"#{name}_does_not_end_with", lambda { |str, sensitive: true|
|
34
|
+
send("#{name}_not_like", "%#{sanitize_sql_like(str)}", sensitive: sensitive)
|
35
|
+
})
|
36
|
+
|
37
|
+
append_scope(model, :"#{name}_matches", lambda { |str, sensitive: true| where(column.matches_regexp(str, sensitive)) })
|
38
|
+
append_scope(model, :"#{name}_does_not_match", lambda { |str, sensitive: true| where(column.does_not_match_regexp(str, sensitive)) })
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
class TypeScopes::Time < TypeScopes
|
2
|
+
def self.types
|
3
|
+
["timestamp", "datetime", "date"].freeze
|
4
|
+
end
|
5
|
+
|
6
|
+
def self.inject_for_column(model, name)
|
7
|
+
short_name = shorten_column_name(name)
|
8
|
+
column = model.arel_table[name]
|
9
|
+
|
10
|
+
append_scope(model, :"#{short_name}_to", lambda { |date| where(column.lteq(date)) })
|
11
|
+
append_scope(model, :"#{short_name}_from", lambda { |date| where(column.gteq(date)) })
|
12
|
+
append_scope(model, :"#{short_name}_after", lambda { |date| where(column.gt(date)) })
|
13
|
+
append_scope(model, :"#{short_name}_before", lambda { |date| where(column.lt(date)) })
|
14
|
+
append_scope(model, :"#{short_name}_between", lambda { |from, to| where(name => from..to) })
|
15
|
+
append_scope(model, :"#{short_name}_not_between", lambda { |from, to| where.not(name => from..to) })
|
16
|
+
append_scope(model, :"#{short_name}_within", lambda { |from, to| where(column.gt(from)).where(column.lt(to)) })
|
17
|
+
append_scope(model, :"#{short_name}_not_within", lambda { |from, to| where(column.lteq(from).or(column.gteq(to))) })
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.shorten_column_name(name)
|
21
|
+
name.chomp("_at").chomp("_on")
|
22
|
+
end
|
23
|
+
end
|
data/lib/type_scopes/version.rb
CHANGED
@@ -1,3 +1,3 @@
|
|
1
|
-
|
2
|
-
VERSION = "0.
|
1
|
+
class TypeScopes
|
2
|
+
VERSION = "0.6.1".freeze
|
3
3
|
end
|
data/lib/type_scopes.rb
CHANGED
@@ -1,13 +1,34 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
1
|
+
class TypeScopes
|
2
|
+
def self.inject(model, column_names = nil)
|
3
|
+
column_names ||= model.table_exists? ? model.columns.map(&:name) : []
|
4
|
+
for name in column_names
|
5
|
+
if column = model.columns_hash[name]
|
6
|
+
Time.support?(column.sql_type) && Time.inject_for_column(model, name)
|
7
|
+
String.support?(column.sql_type) && String.inject_for_column(model, name)
|
8
|
+
Numeric.support?(column.sql_type) && Numeric.inject_for_column(model, name)
|
9
|
+
Boolean.support?(column.sql_type) && Boolean.inject_for_column(model, name)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.append_scope(model, name, block)
|
15
|
+
model.scope(name, block) if !model.respond_to?(name, true)
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.support?(column_type)
|
19
|
+
types.any? { |type| column_type.include?(type) }
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.types
|
23
|
+
raise NotImplementedError
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.inject_for_column(model, name)
|
27
|
+
raise NotImplementedError
|
12
28
|
end
|
13
29
|
end
|
30
|
+
|
31
|
+
require "type_scopes/time"
|
32
|
+
require "type_scopes/string"
|
33
|
+
require "type_scopes/numeric"
|
34
|
+
require "type_scopes/boolean"
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
$LOAD_PATH.unshift(File.expand_path("../../lib", __FILE__))
|
2
|
+
|
3
|
+
require "active_record"
|
4
|
+
require "type_scopes"
|
5
|
+
require "minitest/autorun"
|
6
|
+
|
7
|
+
class TypeScopes::Transaction < ActiveRecord::Base
|
8
|
+
class Migration < ActiveRecord::Migration::Current
|
9
|
+
def up
|
10
|
+
drop_table :transactions, if_exists: true
|
11
|
+
create_table :transactions do |t|
|
12
|
+
t.decimal :amount, null: false
|
13
|
+
t.datetime :paid_at
|
14
|
+
t.string :description
|
15
|
+
t.boolean :non_profit, null: false, default: false
|
16
|
+
t.boolean :is_valid, null: false, default: false
|
17
|
+
t.boolean :has_payment, null: false, default: false
|
18
|
+
t.boolean :was_processed, null: false, default: false
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class TypeScopes::TestCase < Minitest::Test
|
25
|
+
def self.initialize_database
|
26
|
+
# To run against Postgresql set variable : DATABASE_URL=postgres:///type_scopes?user=postgres
|
27
|
+
ActiveRecord::Base.establish_connection(ENV["DATABASE_URL"] || "sqlite3::memory:")
|
28
|
+
ActiveRecord::Migration.verbose = false
|
29
|
+
TypeScopes::Transaction::Migration.new.up
|
30
|
+
TypeScopes.inject TypeScopes::Transaction
|
31
|
+
end
|
32
|
+
|
33
|
+
def sql_adapter_like_case_sensitive?
|
34
|
+
# By default SQLite's like is case insensitive.
|
35
|
+
# So it's not possible to have the exact same tests with other databases.
|
36
|
+
ActiveRecord::Base.connection.adapter_name != "SQLite"
|
37
|
+
end
|
38
|
+
|
39
|
+
def sql_adapter_supports_regex?
|
40
|
+
ActiveRecord::Base.connection.adapter_name != "SQLite"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
TypeScopes::TestCase.initialize_database
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require File.expand_path("../../test_helper", __FILE__)
|
2
|
+
|
3
|
+
class TypeScopes::BooleanTest < TypeScopes::TestCase
|
4
|
+
def setup
|
5
|
+
TypeScopes::Transaction.connection.truncate(TypeScopes::Transaction.table_name)
|
6
|
+
TypeScopes::Transaction.create!(amount: 100, paid_at: "2021-06-23", description: "First transaction")
|
7
|
+
TypeScopes::Transaction.create!(amount: 200, paid_at: "2021-06-24", description: "Last transaction")
|
8
|
+
end
|
9
|
+
|
10
|
+
def test_without_prefix
|
11
|
+
assert_equal(0, TypeScopes::Transaction.non_profit.count)
|
12
|
+
assert_equal(2, TypeScopes::Transaction.not_non_profit.count)
|
13
|
+
end
|
14
|
+
|
15
|
+
def test_has
|
16
|
+
assert_equal(0, TypeScopes::Transaction.has_payment.count)
|
17
|
+
assert_equal(2, TypeScopes::Transaction.has_not_payment.count)
|
18
|
+
end
|
19
|
+
|
20
|
+
def test_is
|
21
|
+
assert_equal(0, TypeScopes::Transaction.is_valid.count)
|
22
|
+
assert_equal(2, TypeScopes::Transaction.is_not_valid.count)
|
23
|
+
end
|
24
|
+
|
25
|
+
def test_was
|
26
|
+
assert_equal(0, TypeScopes::Transaction.was_processed.count)
|
27
|
+
assert_equal(2, TypeScopes::Transaction.was_not_processed.count)
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require File.expand_path("../../test_helper", __FILE__)
|
2
|
+
|
3
|
+
class TypeScopes::NumericTest < TypeScopes::TestCase
|
4
|
+
def setup
|
5
|
+
TypeScopes::Transaction.connection.truncate(TypeScopes::Transaction.table_name)
|
6
|
+
TypeScopes::Transaction.create!(amount: 100)
|
7
|
+
TypeScopes::Transaction.create!(amount: 200)
|
8
|
+
end
|
9
|
+
|
10
|
+
def test_to
|
11
|
+
assert_equal(1, TypeScopes::Transaction.amount_to(199.99).count)
|
12
|
+
assert_equal(2, TypeScopes::Transaction.amount_to(200).count)
|
13
|
+
end
|
14
|
+
|
15
|
+
def test_from
|
16
|
+
assert_equal(2, TypeScopes::Transaction.amount_from(100).count)
|
17
|
+
assert_equal(1, TypeScopes::Transaction.amount_from(100.01).count)
|
18
|
+
end
|
19
|
+
|
20
|
+
def test_above
|
21
|
+
assert_equal(2, TypeScopes::Transaction.amount_above(99.99).count)
|
22
|
+
assert_equal(1, TypeScopes::Transaction.amount_above(100).count)
|
23
|
+
end
|
24
|
+
|
25
|
+
def test_below
|
26
|
+
assert_equal(1, TypeScopes::Transaction.amount_below(200).count)
|
27
|
+
assert_equal(2, TypeScopes::Transaction.amount_below(200.01).count)
|
28
|
+
end
|
29
|
+
|
30
|
+
def test_between
|
31
|
+
assert_equal(2, TypeScopes::Transaction.amount_between(100, 200).count)
|
32
|
+
assert_equal(0, TypeScopes::Transaction.amount_between(100.01, 199.99).count)
|
33
|
+
end
|
34
|
+
|
35
|
+
def test_not_between
|
36
|
+
assert_equal(0, TypeScopes::Transaction.amount_not_between(100, 200).count)
|
37
|
+
assert_equal(2, TypeScopes::Transaction.amount_not_between(100.01, 199.99).count)
|
38
|
+
end
|
39
|
+
|
40
|
+
def test_within
|
41
|
+
assert_equal(0, TypeScopes::Transaction.amount_within(100, 200).count)
|
42
|
+
assert_equal(2, TypeScopes::Transaction.amount_within(99.99, 200.01).count)
|
43
|
+
end
|
44
|
+
|
45
|
+
def test_not_within
|
46
|
+
assert_equal(2, TypeScopes::Transaction.amount_not_within(100, 200).count)
|
47
|
+
assert_equal(0, TypeScopes::Transaction.amount_not_within(99.99, 200.01).count)
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
require File.expand_path("../../test_helper", __FILE__)
|
2
|
+
|
3
|
+
class TypeScopes::StringTest < TypeScopes::TestCase
|
4
|
+
def setup
|
5
|
+
TypeScopes::Transaction.connection.truncate(TypeScopes::Transaction.table_name)
|
6
|
+
TypeScopes::Transaction.create!(amount: 100, paid_at: "2021-06-23", description: "Lorem ipsum")
|
7
|
+
TypeScopes::Transaction.create!(amount: 200, paid_at: "2021-06-24", description: "Lorem ipsum")
|
8
|
+
end
|
9
|
+
|
10
|
+
def test_like
|
11
|
+
assert_equal(2, TypeScopes::Transaction.description_like("%Lorem%").count)
|
12
|
+
return unless sql_adapter_like_case_sensitive?
|
13
|
+
assert_equal(0, TypeScopes::Transaction.description_like("%LOREM%").count)
|
14
|
+
assert_equal(2, TypeScopes::Transaction.description_like("%LOREM%", sensitive: false).count)
|
15
|
+
end
|
16
|
+
|
17
|
+
def test_not_like
|
18
|
+
assert_equal(0, TypeScopes::Transaction.description_not_like("%ipsum").count)
|
19
|
+
return unless sql_adapter_like_case_sensitive?
|
20
|
+
assert_equal(2, TypeScopes::Transaction.description_not_like("%IPSUM").count)
|
21
|
+
assert_equal(0, TypeScopes::Transaction.description_not_like("%IPSUM", sensitive: false).count)
|
22
|
+
end
|
23
|
+
|
24
|
+
def test_ilike
|
25
|
+
assert_equal(0, TypeScopes::Transaction.description_ilike("%xxx%").count)
|
26
|
+
assert_equal(2, TypeScopes::Transaction.description_ilike("LOREM%").count)
|
27
|
+
end
|
28
|
+
|
29
|
+
def test_not_ilike
|
30
|
+
assert_equal(0, TypeScopes::Transaction.description_not_ilike("%IPSUM").count)
|
31
|
+
assert_equal(2, TypeScopes::Transaction.description_not_ilike("%xxx%").count)
|
32
|
+
end
|
33
|
+
|
34
|
+
def test_contains
|
35
|
+
assert_equal(2, TypeScopes::Transaction.description_contains("m i").count)
|
36
|
+
assert_equal(0, TypeScopes::Transaction.description_contains("xxx").count)
|
37
|
+
end
|
38
|
+
|
39
|
+
def test_does_not_contain
|
40
|
+
assert_equal(0, TypeScopes::Transaction.description_does_not_contain("m i").count)
|
41
|
+
assert_equal(2, TypeScopes::Transaction.description_does_not_contain("xxx").count)
|
42
|
+
end
|
43
|
+
|
44
|
+
def test_starts_with
|
45
|
+
assert_equal(2, TypeScopes::Transaction.description_starts_with("Lorem").count)
|
46
|
+
assert_equal(2, TypeScopes::Transaction.description_starts_with("LOREM", sensitive: false).count)
|
47
|
+
return unless sql_adapter_like_case_sensitive?
|
48
|
+
assert_equal(0, TypeScopes::Transaction.description_starts_with("LOREM").count)
|
49
|
+
end
|
50
|
+
|
51
|
+
def test_does_not_start_with
|
52
|
+
assert_equal(0, TypeScopes::Transaction.description_does_not_start_with("Lorem").count)
|
53
|
+
assert_equal(0, TypeScopes::Transaction.description_does_not_start_with("LOREM", sensitive: false).count)
|
54
|
+
return unless sql_adapter_like_case_sensitive?
|
55
|
+
assert_equal(2, TypeScopes::Transaction.description_does_not_start_with("LOREM").count)
|
56
|
+
end
|
57
|
+
|
58
|
+
def test_ends_with
|
59
|
+
assert_equal(2, TypeScopes::Transaction.description_ends_with("ipsum").count)
|
60
|
+
assert_equal(2, TypeScopes::Transaction.description_ends_with("IPSUM", sensitive: false).count)
|
61
|
+
return unless sql_adapter_like_case_sensitive?
|
62
|
+
assert_equal(0, TypeScopes::Transaction.description_ends_with("IPSUM").count)
|
63
|
+
end
|
64
|
+
|
65
|
+
def test_does_not_end_with
|
66
|
+
assert_equal(0, TypeScopes::Transaction.description_does_not_end_with("ipsum").count)
|
67
|
+
assert_equal(0, TypeScopes::Transaction.description_does_not_end_with("IPSUM", sensitive: false).count)
|
68
|
+
return unless sql_adapter_like_case_sensitive?
|
69
|
+
assert_equal(2, TypeScopes::Transaction.description_does_not_end_with("IPSUM").count)
|
70
|
+
end
|
71
|
+
|
72
|
+
def test_escaped_characters
|
73
|
+
assert_equal(0, TypeScopes::Transaction.description_contains("%").count)
|
74
|
+
assert_equal(0, TypeScopes::Transaction.description_contains("_").count)
|
75
|
+
end
|
76
|
+
|
77
|
+
def test_matches
|
78
|
+
skip unless sql_adapter_supports_regex?
|
79
|
+
assert_equal(2, TypeScopes::Transaction.description_matches("Lorem.").count)
|
80
|
+
assert_equal(2, TypeScopes::Transaction.description_matches("LOREM.", sensitive: false).count)
|
81
|
+
assert_equal(0, TypeScopes::Transaction.description_matches("LOREM.").count)
|
82
|
+
end
|
83
|
+
|
84
|
+
def test_does_not_match
|
85
|
+
skip unless sql_adapter_supports_regex?
|
86
|
+
assert_equal(0, TypeScopes::Transaction.description_does_not_match("Lorem.").count)
|
87
|
+
assert_equal(0, TypeScopes::Transaction.description_does_not_match("LOREM.", sensitive: false).count)
|
88
|
+
assert_equal(2, TypeScopes::Transaction.description_does_not_match("LOREM.").count)
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require File.expand_path("../../test_helper", __FILE__)
|
2
|
+
|
3
|
+
class TypeScopes::TimeTest < TypeScopes::TestCase
|
4
|
+
def setup
|
5
|
+
TypeScopes::Transaction.connection.truncate(TypeScopes::Transaction.table_name)
|
6
|
+
TypeScopes::Transaction.create!(amount: 100, paid_at: "2021-06-23")
|
7
|
+
TypeScopes::Transaction.create!(amount: 200, paid_at: "2021-06-24")
|
8
|
+
end
|
9
|
+
|
10
|
+
def test_to
|
11
|
+
assert_equal(1, TypeScopes::Transaction.paid_to("2021-06-23T23:59:59").count)
|
12
|
+
assert_equal(2, TypeScopes::Transaction.paid_to("2021-06-24T00:00:00").count)
|
13
|
+
end
|
14
|
+
|
15
|
+
def test_from
|
16
|
+
assert_equal(2, TypeScopes::Transaction.paid_from("2021-06-23").count)
|
17
|
+
assert_equal(1, TypeScopes::Transaction.paid_from("2021-06-23T00:00:01").count)
|
18
|
+
end
|
19
|
+
|
20
|
+
def test_above
|
21
|
+
assert_equal(2, TypeScopes::Transaction.paid_after("2021-06-22T23:59:59").count)
|
22
|
+
assert_equal(1, TypeScopes::Transaction.paid_after("2021-06-23T00:00:00").count)
|
23
|
+
end
|
24
|
+
|
25
|
+
def test_below
|
26
|
+
assert_equal(1, TypeScopes::Transaction.paid_before("2021-06-24").count)
|
27
|
+
assert_equal(2, TypeScopes::Transaction.paid_before("2021-06-24T00:00:01").count)
|
28
|
+
end
|
29
|
+
|
30
|
+
def test_between
|
31
|
+
assert_equal(2, TypeScopes::Transaction.paid_between("2021-06-23", "2021-06-24T00:00:00").count)
|
32
|
+
assert_equal(0, TypeScopes::Transaction.paid_between("2021-06-23T00:00:01", "2021-06-23T23:59:59").count)
|
33
|
+
end
|
34
|
+
|
35
|
+
def test_not_between
|
36
|
+
assert_equal(0, TypeScopes::Transaction.paid_not_between("2021-06-23", "2021-06-24T00:00:00").count)
|
37
|
+
assert_equal(2, TypeScopes::Transaction.paid_not_between("2021-06-23T00:00:01", "2021-06-23T23:59:59").count)
|
38
|
+
end
|
39
|
+
|
40
|
+
def test_within
|
41
|
+
assert_equal(0, TypeScopes::Transaction.paid_within("2021-06-23T00:00:00", "2021-06-24").count)
|
42
|
+
assert_equal(2, TypeScopes::Transaction.paid_within("2021-06-22T23:59:59", "2021-06-24T:00:00:01").count)
|
43
|
+
end
|
44
|
+
|
45
|
+
def test_not_within
|
46
|
+
assert_equal(2, TypeScopes::Transaction.paid_not_within("2021-06-23T00:00:00", "2021-06-24").count)
|
47
|
+
assert_equal(0, TypeScopes::Transaction.paid_not_within("2021-06-22T23:59:59", "2021-06-24T:00:00:01").count)
|
48
|
+
end
|
49
|
+
end
|
data/type_scopes.gemspec
CHANGED
@@ -8,8 +8,8 @@ Gem::Specification.new do |spec|
|
|
8
8
|
spec.version = TypeScopes::VERSION
|
9
9
|
spec.authors = ["Alexis Bernard"]
|
10
10
|
spec.email = ["alexis@bernard.io"]
|
11
|
-
spec.summary = "
|
12
|
-
spec.description = "
|
11
|
+
spec.summary = "Semantic scopes for your ActiveRecord models."
|
12
|
+
spec.description = "Automatically create semantic scopes based on columns' types (dates, times, strings and numerics)."
|
13
13
|
spec.homepage = "https://github.com/BaseSecrete/type_scopes"
|
14
14
|
spec.license = "MIT"
|
15
15
|
|
metadata
CHANGED
@@ -1,29 +1,39 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: type_scopes
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.6.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Alexis Bernard
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2024-02-22 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
|
-
description:
|
13
|
+
description: Automatically create semantic scopes based on columns' types (dates,
|
14
|
+
times, strings and numerics).
|
14
15
|
email:
|
15
16
|
- alexis@bernard.io
|
16
17
|
executables: []
|
17
18
|
extensions: []
|
18
19
|
extra_rdoc_files: []
|
19
20
|
files:
|
21
|
+
- CHANGELOG.md
|
22
|
+
- Gemfile
|
23
|
+
- Gemfile.lock
|
20
24
|
- README.md
|
21
|
-
-
|
22
|
-
- lib/numeric_scopes.rb
|
23
|
-
- lib/string_scopes.rb
|
24
|
-
- lib/timestamp_scopes.rb
|
25
|
+
- Rakefile
|
25
26
|
- lib/type_scopes.rb
|
27
|
+
- lib/type_scopes/boolean.rb
|
28
|
+
- lib/type_scopes/numeric.rb
|
29
|
+
- lib/type_scopes/string.rb
|
30
|
+
- lib/type_scopes/time.rb
|
26
31
|
- lib/type_scopes/version.rb
|
32
|
+
- test/test_helper.rb
|
33
|
+
- test/type_scopes/boolean_test.rb
|
34
|
+
- test/type_scopes/numeric_test.rb
|
35
|
+
- test/type_scopes/string_test.rb
|
36
|
+
- test/type_scopes/time_test.rb
|
27
37
|
- type_scopes.gemspec
|
28
38
|
homepage: https://github.com/BaseSecrete/type_scopes
|
29
39
|
licenses:
|
@@ -44,8 +54,13 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
44
54
|
- !ruby/object:Gem::Version
|
45
55
|
version: '0'
|
46
56
|
requirements: []
|
47
|
-
rubygems_version: 3.
|
57
|
+
rubygems_version: 3.2.22
|
48
58
|
signing_key:
|
49
59
|
specification_version: 4
|
50
|
-
summary:
|
51
|
-
test_files:
|
60
|
+
summary: Semantic scopes for your ActiveRecord models.
|
61
|
+
test_files:
|
62
|
+
- test/test_helper.rb
|
63
|
+
- test/type_scopes/boolean_test.rb
|
64
|
+
- test/type_scopes/numeric_test.rb
|
65
|
+
- test/type_scopes/string_test.rb
|
66
|
+
- test/type_scopes/time_test.rb
|
data/lib/boolean_scopes.rb
DELETED
@@ -1,29 +0,0 @@
|
|
1
|
-
module BooleanScopes
|
2
|
-
TYPES = ["bool", "boolean", "tinyint(1)"].freeze
|
3
|
-
|
4
|
-
def self.included(model)
|
5
|
-
model.extend(ClassMethods)
|
6
|
-
model.create_boolean_scopes
|
7
|
-
end
|
8
|
-
|
9
|
-
module ClassMethods
|
10
|
-
def create_boolean_scopes
|
11
|
-
for column in columns
|
12
|
-
if TYPES.include?(column.sql_type)
|
13
|
-
create_boolean_scopes_for_column(column.name)
|
14
|
-
end
|
15
|
-
end
|
16
|
-
end
|
17
|
-
|
18
|
-
def create_boolean_scopes_for_column(name)
|
19
|
-
scope :"#{name}", lambda { where(quoted_table_name => { name => true }) }
|
20
|
-
|
21
|
-
prefix, suffix = /\A(has|is|was)_(.+)\z/.match(name).to_a[1..2]
|
22
|
-
if prefix && suffix
|
23
|
-
scope :"#{prefix}_not_#{suffix}", lambda { where(quoted_table_name => { name => false }) }
|
24
|
-
else
|
25
|
-
scope :"not_#{name}", lambda { where(quoted_table_name => { name => false }) }
|
26
|
-
end
|
27
|
-
end
|
28
|
-
end
|
29
|
-
end
|
data/lib/numeric_scopes.rb
DELETED
@@ -1,30 +0,0 @@
|
|
1
|
-
module NumericScopes
|
2
|
-
TYPES = ["integer", "double precision", "numeric", "bigint"].freeze
|
3
|
-
|
4
|
-
def self.included(model)
|
5
|
-
model.extend(ClassMethods)
|
6
|
-
model.create_numeric_scopes
|
7
|
-
end
|
8
|
-
|
9
|
-
module ClassMethods
|
10
|
-
def create_numeric_scopes
|
11
|
-
for column in columns
|
12
|
-
if TYPES.any? { |type| column.sql_type.include?(type) }
|
13
|
-
create_numeric_scopes_for_column(column.name)
|
14
|
-
end
|
15
|
-
end
|
16
|
-
end
|
17
|
-
|
18
|
-
def create_numeric_scopes_for_column(name)
|
19
|
-
full_name = "#{quoted_table_name}.#{name}"
|
20
|
-
scope :"#{name}_to", lambda { |value| where("#{full_name} <= ?", value) }
|
21
|
-
scope :"#{name}_from", lambda { |value| where("#{full_name} >= ?", value) }
|
22
|
-
scope :"#{name}_above", lambda { |value| where("#{full_name} > ?", value) }
|
23
|
-
scope :"#{name}_below", lambda { |value| where("#{full_name} < ?", value) }
|
24
|
-
scope :"#{name}_between", lambda { |from, to| where("#{full_name} BETWEEN ? AND ?", from, to) }
|
25
|
-
scope :"#{name}_not_between", lambda { |from, to| where("#{full_name} BETWEEN ? AND ?", from, to) }
|
26
|
-
scope :"#{name}_within", lambda { |from, to| where("#{full_name} > ? AND #{full_name} < ?", from, to) }
|
27
|
-
scope :"#{name}_not_within", lambda { |from, to| where("#{full_name} <= ? OR #{full_name} >= ?", from, to) }
|
28
|
-
end
|
29
|
-
end
|
30
|
-
end
|
data/lib/string_scopes.rb
DELETED
@@ -1,31 +0,0 @@
|
|
1
|
-
module StringScopes
|
2
|
-
TYPES = ["character", "text"].freeze
|
3
|
-
|
4
|
-
def self.included(model)
|
5
|
-
model.extend(ClassMethods)
|
6
|
-
model.create_string_scopes
|
7
|
-
end
|
8
|
-
|
9
|
-
def self.escape(string)
|
10
|
-
string = string.gsub("%".freeze, "[%]".freeze)
|
11
|
-
string.gsub!("_".freeze, "[_]".freeze)
|
12
|
-
string
|
13
|
-
end
|
14
|
-
|
15
|
-
module ClassMethods
|
16
|
-
def create_string_scopes
|
17
|
-
for column in columns
|
18
|
-
if TYPES.any? { |type| column.sql_type.include?(type) }
|
19
|
-
create_string_scopes_for_column(column.name)
|
20
|
-
end
|
21
|
-
end
|
22
|
-
end
|
23
|
-
|
24
|
-
def create_string_scopes_for_column(name)
|
25
|
-
full_name = "#{quoted_table_name}.#{name}"
|
26
|
-
scope :"#{name}_contains", lambda { |str| where("#{full_name} LIKE ?", "%#{StringScopes.escape(str)}%") }
|
27
|
-
scope :"#{name}_starts_with", lambda { |str| where("#{full_name} LIKE ?", "#{StringScopes.escape(str)}%") }
|
28
|
-
scope :"#{name}_ends_with", lambda { |str| where("#{full_name} LIKE ?", "%#{StringScopes.escape(str)}") }
|
29
|
-
end
|
30
|
-
end
|
31
|
-
end
|
data/lib/timestamp_scopes.rb
DELETED
@@ -1,35 +0,0 @@
|
|
1
|
-
module TimestampScopes
|
2
|
-
TYPES = ["timestamp", "datetime", "date"].freeze
|
3
|
-
|
4
|
-
def self.included(model)
|
5
|
-
model.extend(ClassMethods)
|
6
|
-
model.create_timestamp_scopes
|
7
|
-
end
|
8
|
-
|
9
|
-
module ClassMethods
|
10
|
-
def create_timestamp_scopes
|
11
|
-
for column in columns
|
12
|
-
if TYPES.any? { |type| column.sql_type.include?(type) }
|
13
|
-
create_timestamp_scopes_for_column(column.name)
|
14
|
-
end
|
15
|
-
end
|
16
|
-
end
|
17
|
-
|
18
|
-
def create_timestamp_scopes_for_column(name)
|
19
|
-
full_name = "#{quoted_table_name}.#{name}"
|
20
|
-
short_name = shorten_column_name(name)
|
21
|
-
scope :"#{short_name}_to", lambda { |date| where("#{full_name} <= ?", date) }
|
22
|
-
scope :"#{short_name}_from", lambda { |date| where("#{full_name} >= ?", date) }
|
23
|
-
scope :"#{short_name}_after", lambda { |date| where("#{full_name} > ?", date) }
|
24
|
-
scope :"#{short_name}_before", lambda { |date| where("#{full_name} < ?", date) }
|
25
|
-
scope :"#{short_name}_between", lambda { |from, to| where("#{full_name} BETWEEN ? AND ?", from, to) }
|
26
|
-
scope :"#{short_name}_not_between", lambda { |from, to| where("#{full_name} NOT BETWEEN ? AND ?", from, to) }
|
27
|
-
scope :"#{short_name}_within", lambda { |from, to| where("#{full_name} > ? AND #{full_name} < ?", from, to) }
|
28
|
-
scope :"#{short_name}_not_within", lambda { |from, to| where("#{full_name} <= ? OR #{full_name} >= ?", from, to) }
|
29
|
-
end
|
30
|
-
|
31
|
-
def shorten_column_name(name)
|
32
|
-
name.chomp("_at").chomp("_on")
|
33
|
-
end
|
34
|
-
end
|
35
|
-
end
|