type_scopes 0.1.0 → 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|