type_scopes 0.1.0 → 0.6.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 +5 -5
- data/Gemfile +6 -0
- data/Gemfile.lock +36 -0
- data/README.md +48 -24
- 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 +50 -0
- data/lib/type_scopes/time.rb +23 -0
- data/lib/type_scopes/version.rb +2 -2
- data/lib/type_scopes.rb +30 -8
- 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 +24 -11
- data/lib/numeric_scopes.rb +0 -24
- data/lib/string_scopes.rb +0 -28
- data/lib/timestamp_scopes.rb +0 -29
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: d3ac3e90478e5e0aca44a37d9422d212af77ea01939072b12845d19f139ba3e2
|
4
|
+
data.tar.gz: 3fd3b0d8964ed751f3c304c91e0298ee4d4d8eb56a325ccb350193d4f92ea412
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 322664b363b060bed5a744449975593bbb28c2b01a67b614a0dbc3325aea7f9ba57c3b048db250ff4bf2fbf515e28ed4d657b5744a2634bb27052232fc06c2cb
|
7
|
+
data.tar.gz: eb7388daf228f71ce4437134cb78b1369e8e75214d6f147c76e28aa71aac454e425cb5624c2ed434e5c816aca343bc946ff87cb9a6b3a77d31190e37b0c10ae4
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
GEM
|
2
|
+
remote: https://rubygems.org/
|
3
|
+
specs:
|
4
|
+
activemodel (6.1.3.2)
|
5
|
+
activesupport (= 6.1.3.2)
|
6
|
+
activerecord (6.1.3.2)
|
7
|
+
activemodel (= 6.1.3.2)
|
8
|
+
activesupport (= 6.1.3.2)
|
9
|
+
activesupport (6.1.3.2)
|
10
|
+
concurrent-ruby (~> 1.0, >= 1.0.2)
|
11
|
+
i18n (>= 1.6, < 2)
|
12
|
+
minitest (>= 5.1)
|
13
|
+
tzinfo (~> 2.0)
|
14
|
+
zeitwerk (~> 2.3)
|
15
|
+
concurrent-ruby (1.1.9)
|
16
|
+
i18n (1.8.10)
|
17
|
+
concurrent-ruby (~> 1.0)
|
18
|
+
minitest (5.14.4)
|
19
|
+
pg (1.2.3)
|
20
|
+
rake (13.0.3)
|
21
|
+
sqlite3 (1.4.2)
|
22
|
+
tzinfo (2.0.4)
|
23
|
+
concurrent-ruby (~> 1.0)
|
24
|
+
zeitwerk (2.4.2)
|
25
|
+
|
26
|
+
PLATFORMS
|
27
|
+
ruby
|
28
|
+
|
29
|
+
DEPENDENCIES
|
30
|
+
activerecord (>= 6.1.3.2)
|
31
|
+
pg
|
32
|
+
rake
|
33
|
+
sqlite3
|
34
|
+
|
35
|
+
BUNDLED WITH
|
36
|
+
2.1.4
|
data/README.md
CHANGED
@@ -1,24 +1,27 @@
|
|
1
1
|
# Type Scopes
|
2
2
|
|
3
|
-
Type scopes creates useful scopes based on the type of the columns of your models.
|
4
|
-
It handles dates, times, strings and
|
3
|
+
Type scopes creates useful semantic scopes based on the type of the columns of your models.
|
4
|
+
It handles dates, times, strings, numerics and booleans.
|
5
5
|
|
6
|
-
Here
|
6
|
+
Here are examples for all the available scopes:
|
7
7
|
|
8
8
|
```ruby
|
9
9
|
# paid_at: datetime
|
10
10
|
# amount: decimal
|
11
11
|
# description: string
|
12
12
|
class Transaction < ActiveRecord::Base
|
13
|
-
|
13
|
+
TypeScopes.inject self
|
14
14
|
end
|
15
15
|
|
16
16
|
# Time scopes
|
17
|
-
Transaction.paid_to("2017-09-06") # => where("
|
18
|
-
Transaction.paid_from("2017-09-06") # => where("
|
19
|
-
Transaction.paid_after("2017-09-06") # => where("
|
20
|
-
Transaction.paid_before("2017-09-06") #= where("
|
21
|
-
Transaction.paid_between("2017-09-06", "2017-09-07") # => where("
|
17
|
+
Transaction.paid_to("2017-09-06") # => where("paid_at <= '2017-09-06'")
|
18
|
+
Transaction.paid_from("2017-09-06") # => where("paid_at >= '2017-09-06'")
|
19
|
+
Transaction.paid_after("2017-09-06") # => where("paid_at > '2017-09-06'")
|
20
|
+
Transaction.paid_before("2017-09-06") #= where("paid_at < '2017-09-06'")
|
21
|
+
Transaction.paid_between("2017-09-06", "2017-09-07") # => where("paid_at BETWEEN '2017-09-06' AND '2017-09-07'")
|
22
|
+
Transaction.paid_not_between("2017-09-06", "2017-09-07") # => where("paid_at NOT BETWEEN '2017-09-06' AND '2017-09-07'")
|
23
|
+
Transaction.paid_within("2017-09-06", "2017-09-07") # => where("paid_at > '2017-09-06' AND paid_at < '2017-09-07'")
|
24
|
+
Transaction.paid_not_within("2017-09-06", "2017-09-07") # => where("paid_at <= '2017-09-06' OR paid_at >= '2017-09-07'")
|
22
25
|
|
23
26
|
# Numeric scopes
|
24
27
|
Transaction.amount_to(100) # => where("amount <= 100")
|
@@ -26,14 +29,40 @@ Transaction.amount_from(100) # => where("amount >= 100")
|
|
26
29
|
Transaction.amount_above(100) # => where("amount > 100")
|
27
30
|
Transaction.amount_below(100) # => where("amount < 100")
|
28
31
|
Transaction.amount_between(100, 200) # => where("amount BETWEEN 100 AND 200")
|
32
|
+
Transaction.amount_not_between(100, 200) # => where("amount NOT BETWEEN 100 AND 200")
|
33
|
+
Transaction.amount_within(100, 200) # => where("amount > 100 AND amount < 200")
|
34
|
+
Transaction.amount_not_within(100, 200) # => where("amount <= 100 OR amount >= 200")
|
29
35
|
|
30
36
|
# String scopes
|
31
37
|
Transaction.description_contains("foo") # => where("description LIKE '%foo%'")
|
38
|
+
Transaction.description_contains("foo", sensitive: false) # => where("description ILIKE '%foo%'")
|
32
39
|
Transaction.description_starts_with("foo") # => where("description LIKE 'foo%'")
|
40
|
+
Transaction.description_starts_with("foo", sensitive: false) # => where("description ILIKE 'foo%'")
|
41
|
+
Transaction.description_does_not_start_with("foo") # => where("description NOT LIKE 'foo%'")
|
42
|
+
Transaction.description_does_not_start_with("foo", sensitive: false) # => where("description NOT ILIKE 'foo%'")
|
33
43
|
Transaction.description_ends_with("foo") # => where("description LIKE '%foo'")
|
44
|
+
Transaction.description_ends_with("foo", sensitive: false) # => where("description ILIKE '%foo'")
|
45
|
+
Transaction.description_does_not_end_with("foo") # => where("description NOT LIKE '%foo'")
|
46
|
+
Transaction.description_does_not_end_with("foo", sensitive: false) # => where("description NOT ILIKE '%foo'")
|
47
|
+
Transaction.description_like("%foo%") # => where("description LIKE '%foo%'")
|
48
|
+
Transaction.description_not_like("%foo%") # => where("description NOT LIKE '%foo%'")
|
49
|
+
Transaction.description_ilike("%foo%") # => where("description ILIKE '%foo%'")
|
50
|
+
Transaction.description_not_ilike("%foo%") # => where("description NOT ILIKE '%foo%'")
|
51
|
+
Transaction.description_matches("^Regex$") # => where("description ~ '^Regex$'")
|
52
|
+
Transaction.description_does_not_match("^Regex$") # => where("description !~ '^Regex$'")
|
53
|
+
|
54
|
+
# Boolean scopes
|
55
|
+
Transaction.non_profit # => where("non_profit = true")
|
56
|
+
Transaction.not_non_profit # => where("non_profit = false")
|
57
|
+
Transaction.is_valid # => where("is_valid = true")
|
58
|
+
Transaction.is_not_valid # => where("is_valid = false")
|
59
|
+
Transaction.has_payment # => where("has_payment = true")
|
60
|
+
Transaction.has_not_payment # => where("has_payment = false")
|
61
|
+
Transaction.was_processed # => where("was_processed = true")
|
62
|
+
Transaction.was_not_processed # => where("was_processed = false")
|
34
63
|
```
|
35
64
|
|
36
|
-
For the string
|
65
|
+
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.
|
37
66
|
|
38
67
|
```ruby
|
39
68
|
Transaction.description_contains("%foo_") # => where("description LIKE '%[%]foo[_]%'")
|
@@ -41,26 +70,21 @@ Transaction.description_contains("%foo_") # => where("description LIKE '%[%]foo[
|
|
41
70
|
|
42
71
|
## Install
|
43
72
|
|
44
|
-
Add to your Gemfile:
|
73
|
+
Add to your Gemfile `gem "type_scopes"` and run in your terminal `bundle install`. Then call `TypeScopes.inject` from your models:
|
45
74
|
|
46
75
|
```ruby
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
```shell
|
53
|
-
bundle install
|
54
|
-
```
|
76
|
+
# /app/models/transaction.rb
|
77
|
+
class Transaction < ApplicationRecord
|
78
|
+
# Creates scope for all supported column types
|
79
|
+
TypeScopes.inject self
|
55
80
|
|
56
|
-
|
57
|
-
|
58
|
-
```ruby
|
59
|
-
class Transaction < ActiveRecord::Base
|
60
|
-
include TypeScopes
|
81
|
+
# Or if you prefer to enable scopes for specific columns only
|
82
|
+
TypeScopes.inject self, :amount, :paid_at
|
61
83
|
end
|
62
84
|
```
|
63
85
|
|
86
|
+
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.
|
87
|
+
|
64
88
|
## MIT License
|
65
89
|
|
66
90
|
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,50 @@
|
|
1
|
+
class TypeScopes::String < TypeScopes
|
2
|
+
def self.types
|
3
|
+
["character", "text", "varchar"].freeze
|
4
|
+
end
|
5
|
+
|
6
|
+
def self.escape(string)
|
7
|
+
string = string.gsub("%".freeze, "[%]".freeze)
|
8
|
+
string.gsub!("_".freeze, "[_]".freeze)
|
9
|
+
string
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.inject_for_column(model, name)
|
13
|
+
column = model.arel_table[name]
|
14
|
+
append_scope(model, :"#{name}_like", lambda { |str, sensitive: true| where(column.matches(str, nil, sensitive)) })
|
15
|
+
append_scope(model, :"#{name}_not_like", lambda { |str, sensitive: true| where(column.does_not_match(str, nil, sensitive)) })
|
16
|
+
append_scope(model, :"#{name}_ilike", lambda { |str| where(column.matches(str)) })
|
17
|
+
append_scope(model, :"#{name}_not_ilike", lambda { |str| where(column.does_not_match(str)) })
|
18
|
+
|
19
|
+
append_scope(model, :"#{name}_contains", lambda { |str, sensitive: true|
|
20
|
+
send("#{name}_like", "%#{TypeScopes::String.escape(str)}%", sensitive: sensitive)
|
21
|
+
})
|
22
|
+
|
23
|
+
append_scope(model, :"#{name}_does_not_contain", lambda { |str, sensitive: true|
|
24
|
+
send("#{name}_not_like", "%#{TypeScopes::String.escape(str)}%", sensitive: sensitive)
|
25
|
+
})
|
26
|
+
|
27
|
+
append_scope(model, :"#{name}_does_not_contain", lambda { |str, sensitive: true|
|
28
|
+
send("#{name}_like", "%#{TypeScopes::String.escape(str)}%", sensitive: sensitive)
|
29
|
+
})
|
30
|
+
|
31
|
+
append_scope(model, :"#{name}_starts_with", lambda { |str, sensitive: true|
|
32
|
+
send("#{name}_like", "#{TypeScopes::String.escape(str)}%", sensitive: sensitive)
|
33
|
+
})
|
34
|
+
|
35
|
+
append_scope(model, :"#{name}_does_not_start_with", lambda { |str, sensitive: true|
|
36
|
+
send("#{name}_not_like", "#{TypeScopes::String.escape(str)}%", sensitive: sensitive)
|
37
|
+
})
|
38
|
+
|
39
|
+
append_scope(model, :"#{name}_ends_with", lambda { |str, sensitive: true|
|
40
|
+
send("#{name}_like", "%#{TypeScopes::String.escape(str)}", sensitive: sensitive)
|
41
|
+
})
|
42
|
+
|
43
|
+
append_scope(model, :"#{name}_does_not_end_with", lambda { |str, sensitive: true|
|
44
|
+
send("#{name}_not_like", "%#{TypeScopes::String.escape(str)}", sensitive: sensitive)
|
45
|
+
})
|
46
|
+
|
47
|
+
append_scope(model, :"#{name}_matches", lambda { |str, sensitive: true| where(column.matches_regexp(str, sensitive)) })
|
48
|
+
append_scope(model, :"#{name}_does_not_match", lambda { |str, sensitive: true| where(column.does_not_match_regexp(str, sensitive)) })
|
49
|
+
end
|
50
|
+
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.0".freeze
|
3
3
|
end
|
data/lib/type_scopes.rb
CHANGED
@@ -1,11 +1,33 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
1
|
+
class TypeScopes
|
2
|
+
def self.inject(model, column_names = model.columns.map(&:name))
|
3
|
+
for name in column_names
|
4
|
+
if column = model.columns_hash[name]
|
5
|
+
Time.support?(column.sql_type) && Time.inject_for_column(model, name)
|
6
|
+
String.support?(column.sql_type) && String.inject_for_column(model, name)
|
7
|
+
Numeric.support?(column.sql_type) && Numeric.inject_for_column(model, name)
|
8
|
+
Boolean.support?(column.sql_type) && Boolean.inject_for_column(model, name)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.append_scope(model, name, block)
|
14
|
+
model.scope(name, block) if !model.respond_to?(name, true)
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.support?(column_type)
|
18
|
+
types.any? { |type| column_type.include?(type) }
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.types
|
22
|
+
raise NotImplementedError
|
23
|
+
end
|
4
24
|
|
5
|
-
|
6
|
-
|
7
|
-
model.include(StringScopes)
|
8
|
-
model.include(NumericScopes)
|
9
|
-
model.include(TimestampScopes)
|
25
|
+
def self.inject_for_column(model, name)
|
26
|
+
raise NotImplementedError
|
10
27
|
end
|
11
28
|
end
|
29
|
+
|
30
|
+
require "type_scopes/time"
|
31
|
+
require "type_scopes/string"
|
32
|
+
require "type_scopes/numeric"
|
33
|
+
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,28 +1,38 @@
|
|
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.0
|
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: 2021-10-15 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
|
+
- Gemfile
|
22
|
+
- Gemfile.lock
|
20
23
|
- README.md
|
21
|
-
-
|
22
|
-
- lib/string_scopes.rb
|
23
|
-
- lib/timestamp_scopes.rb
|
24
|
+
- Rakefile
|
24
25
|
- lib/type_scopes.rb
|
26
|
+
- lib/type_scopes/boolean.rb
|
27
|
+
- lib/type_scopes/numeric.rb
|
28
|
+
- lib/type_scopes/string.rb
|
29
|
+
- lib/type_scopes/time.rb
|
25
30
|
- lib/type_scopes/version.rb
|
31
|
+
- test/test_helper.rb
|
32
|
+
- test/type_scopes/boolean_test.rb
|
33
|
+
- test/type_scopes/numeric_test.rb
|
34
|
+
- test/type_scopes/string_test.rb
|
35
|
+
- test/type_scopes/time_test.rb
|
26
36
|
- type_scopes.gemspec
|
27
37
|
homepage: https://github.com/BaseSecrete/type_scopes
|
28
38
|
licenses:
|
@@ -43,10 +53,13 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
43
53
|
- !ruby/object:Gem::Version
|
44
54
|
version: '0'
|
45
55
|
requirements: []
|
46
|
-
|
47
|
-
rubygems_version: 2.4.8
|
56
|
+
rubygems_version: 3.1.6
|
48
57
|
signing_key:
|
49
58
|
specification_version: 4
|
50
|
-
summary:
|
51
|
-
test_files:
|
52
|
-
|
59
|
+
summary: Semantic scopes for your ActiveRecord models.
|
60
|
+
test_files:
|
61
|
+
- test/test_helper.rb
|
62
|
+
- test/type_scopes/boolean_test.rb
|
63
|
+
- test/type_scopes/numeric_test.rb
|
64
|
+
- test/type_scopes/string_test.rb
|
65
|
+
- test/type_scopes/time_test.rb
|
data/lib/numeric_scopes.rb
DELETED
@@ -1,24 +0,0 @@
|
|
1
|
-
module NumericScopes
|
2
|
-
def self.included(model)
|
3
|
-
model.extend(ClassMethods)
|
4
|
-
model.create_numeric_scopes
|
5
|
-
end
|
6
|
-
|
7
|
-
module ClassMethods
|
8
|
-
def create_numeric_scopes
|
9
|
-
for column in columns
|
10
|
-
if column.sql_type.index("integer") == 0
|
11
|
-
create_numeric_scopes_for_column(column.name)
|
12
|
-
end
|
13
|
-
end
|
14
|
-
end
|
15
|
-
|
16
|
-
def create_numeric_scopes_for_column(name)
|
17
|
-
scope :"#{name}_to", lambda { |value| where("#{quoted_table_name}.#{name} <= ?", value) }
|
18
|
-
scope :"#{name}_from", lambda { |value| where("#{quoted_table_name}.#{name} >= ?", value) }
|
19
|
-
scope :"#{name}_above", lambda { |value| where("#{quoted_table_name}.#{name} > ?", value) }
|
20
|
-
scope :"#{name}_below", lambda { |value| where("#{quoted_table_name}.#{name} < ?", value) }
|
21
|
-
scope :"#{name}_between", lambda { |from, to| where("#{quoted_table_name}.#{name} BETWEEN ? AND ?", from, to) }
|
22
|
-
end
|
23
|
-
end
|
24
|
-
end
|
data/lib/string_scopes.rb
DELETED
@@ -1,28 +0,0 @@
|
|
1
|
-
module StringScopes
|
2
|
-
def self.included(model)
|
3
|
-
model.extend(ClassMethods)
|
4
|
-
model.create_string_scopes
|
5
|
-
end
|
6
|
-
|
7
|
-
def self.escape(string)
|
8
|
-
string = string.gsub("%".freeze, "[%]".freeze)
|
9
|
-
string.gsub!("_".freeze, "[_]".freeze)
|
10
|
-
string
|
11
|
-
end
|
12
|
-
|
13
|
-
module ClassMethods
|
14
|
-
def create_string_scopes
|
15
|
-
for column in columns
|
16
|
-
if column.sql_type.index("character") == 0 || column.sql_type.index("text") == 0
|
17
|
-
create_string_scopes_for_column(column.name)
|
18
|
-
end
|
19
|
-
end
|
20
|
-
end
|
21
|
-
|
22
|
-
def create_string_scopes_for_column(name)
|
23
|
-
scope :"#{name}_contains", lambda { |str| where("#{quoted_table_name}.#{name} LIKE ?", "%#{StringScopes.escape(str)}%") }
|
24
|
-
scope :"#{name}_starts_with", lambda { |str| where("#{quoted_table_name}.#{name} LIKE ?", "#{StringScopes.escape(str)}%") }
|
25
|
-
scope :"#{name}_ends_with", lambda { |str| where("#{quoted_table_name}.#{name} LIKE ?", "%#{StringScopes.escape(str)}") }
|
26
|
-
end
|
27
|
-
end
|
28
|
-
end
|
data/lib/timestamp_scopes.rb
DELETED
@@ -1,29 +0,0 @@
|
|
1
|
-
module TimestampScopes
|
2
|
-
def self.included(model)
|
3
|
-
model.extend(ClassMethods)
|
4
|
-
model.create_timestamp_scopes
|
5
|
-
end
|
6
|
-
|
7
|
-
module ClassMethods
|
8
|
-
def create_timestamp_scopes
|
9
|
-
for column in columns
|
10
|
-
if column.sql_type.index("timestamp") == 0
|
11
|
-
create_timestamp_scopes_for_column(column.name)
|
12
|
-
end
|
13
|
-
end
|
14
|
-
end
|
15
|
-
|
16
|
-
def create_timestamp_scopes_for_column(name)
|
17
|
-
short_name = shorten_column_name(name)
|
18
|
-
scope :"#{short_name}_to", lambda { |date| where("#{quoted_table_name}.#{name} <= ?", date) }
|
19
|
-
scope :"#{short_name}_from", lambda { |date| where("#{quoted_table_name}.#{name} >= ?", date) }
|
20
|
-
scope :"#{short_name}_after", lambda { |date| where("#{quoted_table_name}.#{name} > ?", date) }
|
21
|
-
scope :"#{short_name}_before", lambda { |date| where("#{quoted_table_name}.#{name} < ?", date) }
|
22
|
-
scope :"#{short_name}_between", lambda { |from, to| where("#{quoted_table_name}.#{name} BETWEEN ? AND ?", from, to) }
|
23
|
-
end
|
24
|
-
|
25
|
-
def shorten_column_name(name)
|
26
|
-
name.chomp("_at").chomp("_on")
|
27
|
-
end
|
28
|
-
end
|
29
|
-
end
|