type_scopes 0.4.0 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9e8bdc381d58fe45f3469a665b2f5be91c2dd26a3865152eaf121b01d557faa5
4
- data.tar.gz: 6d1cba071587a8ede1cdf4689bd5cc5306c63316ebc9dce7b4f5fa29bedfdd6f
3
+ metadata.gz: d3ac3e90478e5e0aca44a37d9422d212af77ea01939072b12845d19f139ba3e2
4
+ data.tar.gz: 3fd3b0d8964ed751f3c304c91e0298ee4d4d8eb56a325ccb350193d4f92ea412
5
5
  SHA512:
6
- metadata.gz: 779176bcded65c9163253892abc66d5df1d7443bc6f1a2c6d15843cf72af3e968d0a6e922507dfb883484c1faa7dc83c2b18b361c98d21e05e3831b3a5d5f23e
7
- data.tar.gz: 3772b0545a904a4b6cbe7bd0677bab5f5f341af5b9f5a15ff6857f1ee4a19f6bf88abfa26df041ffa1e74335c6e1c091807febf99f0976b9efbbb19b8deec9cd
6
+ metadata.gz: 322664b363b060bed5a744449975593bbb28c2b01a67b614a0dbc3325aea7f9ba57c3b048db250ff4bf2fbf515e28ed4d657b5744a2634bb27052232fc06c2cb
7
+ data.tar.gz: eb7388daf228f71ce4437134cb78b1369e8e75214d6f147c76e28aa71aac454e425cb5624c2ed434e5c816aca343bc946ff87cb9a6b3a77d31190e37b0c10ae4
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem "rake"
4
+ gem "activerecord", ">= 6.1.3.2"
5
+ gem "sqlite3"
6
+ gem "pg"
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,7 +1,7 @@
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 numerics.
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
6
  Here are examples for all the available scopes:
7
7
 
@@ -10,7 +10,7 @@ Here are examples for all the available scopes:
10
10
  # amount: decimal
11
11
  # description: string
12
12
  class Transaction < ActiveRecord::Base
13
- include TypeScopes
13
+ TypeScopes.inject self
14
14
  end
15
15
 
16
16
  # Time scopes
@@ -35,8 +35,21 @@ Transaction.amount_not_within(100, 200) # => where("amount <= 100 OR amount >= 2
35
35
 
36
36
  # String scopes
37
37
  Transaction.description_contains("foo") # => where("description LIKE '%foo%'")
38
+ Transaction.description_contains("foo", sensitive: false) # => where("description ILIKE '%foo%'")
38
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%'")
39
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$'")
40
53
 
41
54
  # Boolean scopes
42
55
  Transaction.non_profit # => where("non_profit = true")
@@ -49,7 +62,7 @@ Transaction.was_processed # => where("was_processed = true")
49
62
  Transaction.was_not_processed # => where("was_processed = false")
50
63
  ```
51
64
 
52
- For the string scope the pattern matching is escaped:
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.
53
66
 
54
67
  ```ruby
55
68
  Transaction.description_contains("%foo_") # => where("description LIKE '%[%]foo[_]%'")
@@ -57,26 +70,21 @@ Transaction.description_contains("%foo_") # => where("description LIKE '%[%]foo[
57
70
 
58
71
  ## Install
59
72
 
60
- 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:
61
74
 
62
75
  ```ruby
63
- gem "type_scopes"
64
- ```
65
-
66
- And run in your terminal:
67
-
68
- ```shell
69
- bundle install
70
- ```
76
+ # /app/models/transaction.rb
77
+ class Transaction < ApplicationRecord
78
+ # Creates scope for all supported column types
79
+ TypeScopes.inject self
71
80
 
72
- Then include TypeScopes from your models:
73
-
74
- ```ruby
75
- class Transaction < ActiveRecord::Base
76
- include TypeScopes
81
+ # Or if you prefer to enable scopes for specific columns only
82
+ TypeScopes.inject self, :amount, :paid_at
77
83
  end
78
84
  ```
79
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
+
80
88
  ## MIT License
81
89
 
82
90
  Made by [Base Secrète](https://basesecrete.com/en).
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ require "rake/testtask"
2
+
3
+ Rake::TestTask.new :test do |t|
4
+ t.libs = ["lib", "test"]
5
+ t.pattern = "test/**/*_test.rb"
6
+ end
7
+
8
+ task default: :test
@@ -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
@@ -1,3 +1,3 @@
1
- module TypeScopes
2
- VERSION = "0.4.0".freeze
1
+ class TypeScopes
2
+ VERSION = "0.6.0".freeze
3
3
  end
data/lib/type_scopes.rb CHANGED
@@ -1,13 +1,33 @@
1
- require "string_scopes"
2
- require "numeric_scopes"
3
- require "timestamp_scopes"
4
- require "boolean_scopes"
5
-
6
- module TypeScopes
7
- def self.included(model)
8
- model.include(StringScopes)
9
- model.include(NumericScopes)
10
- model.include(TimestampScopes)
11
- model.include(BooleanScopes)
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
24
+
25
+ def self.inject_for_column(model, name)
26
+ raise NotImplementedError
12
27
  end
13
28
  end
29
+
30
+ require "type_scopes/time"
31
+ require "type_scopes/string"
32
+ require "type_scopes/numeric"
33
+ require "type_scopes/boolean"
@@ -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 = "Automatic scopes for ActiveRecord models."
12
- spec.description = "Useful scopes based on columns' types (dates, times, strings and numerics)."
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,38 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: type_scopes
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.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: 2021-06-22 00:00:00.000000000 Z
11
+ date: 2021-10-15 00:00:00.000000000 Z
12
12
  dependencies: []
13
- description: Useful scopes based on columns' types (dates, times, strings and numerics).
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
- - lib/boolean_scopes.rb
22
- - lib/numeric_scopes.rb
23
- - lib/string_scopes.rb
24
- - lib/timestamp_scopes.rb
24
+ - Rakefile
25
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
26
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
27
36
  - type_scopes.gemspec
28
37
  homepage: https://github.com/BaseSecrete/type_scopes
29
38
  licenses:
@@ -44,8 +53,13 @@ required_rubygems_version: !ruby/object:Gem::Requirement
44
53
  - !ruby/object:Gem::Version
45
54
  version: '0'
46
55
  requirements: []
47
- rubygems_version: 3.0.3
56
+ rubygems_version: 3.1.6
48
57
  signing_key:
49
58
  specification_version: 4
50
- summary: Automatic scopes for ActiveRecord models.
51
- test_files: []
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
@@ -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
@@ -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
@@ -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