type_scopes 0.4.0 → 0.6.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9e8bdc381d58fe45f3469a665b2f5be91c2dd26a3865152eaf121b01d557faa5
4
- data.tar.gz: 6d1cba071587a8ede1cdf4689bd5cc5306c63316ebc9dce7b4f5fa29bedfdd6f
3
+ metadata.gz: 47e7400e39752e376e84d65854ac9eb4130505a6ee311aad99c7d5c132dceb02
4
+ data.tar.gz: 7a6d6e7d9dbe5d0c07771f175084c0727d3733fcf3ddafa3cd20b8cc50f3b94a
5
5
  SHA512:
6
- metadata.gz: 779176bcded65c9163253892abc66d5df1d7443bc6f1a2c6d15843cf72af3e968d0a6e922507dfb883484c1faa7dc83c2b18b361c98d21e05e3831b3a5d5f23e
7
- data.tar.gz: 3772b0545a904a4b6cbe7bd0677bab5f5f341af5b9f5a15ff6857f1ee4a19f6bf88abfa26df041ffa1e74335c6e1c091807febf99f0976b9efbbb19b8deec9cd
6
+ metadata.gz: 8f8d0cb2bf88623c23ac42f3bfc763fbbc2dfa75a741a7a0e92ffedc479cb0e5181de44397047c85719eef0f4bc831835d06cbf7f048b2f8bcc99200c5ffabb0
7
+ data.tar.gz: 5ec206a432eaa902c84d2b7ad535e8422ca2394eebd32fd04c17005ba6fb71b9e4097806a112be602ac1408124904293f939bca43370daa2d0ca5d8d60c0ce47
data/CHANGELOG.md ADDED
@@ -0,0 +1,17 @@
1
+ # Changelog of Type scopes
2
+
3
+ ## 0.6.1 (2024-02-22)
4
+
5
+ - Check if table exists to prevent from ActiveRecord::StatementInvalid
6
+
7
+ ## 0.6 (2021-10-15)
8
+
9
+ - Refactor by switching modules into classes.
10
+
11
+ New usage is `TypeScopes.inject` instead of `include` :
12
+
13
+ ```ruby
14
+ class Model < ApplicationRecord
15
+ TypeScopes.inject(self)
16
+ end
17
+ ```
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem "rake"
4
+ gem "activerecord"
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 (7.0.4.1)
5
+ activesupport (= 7.0.4.1)
6
+ activerecord (7.0.4.1)
7
+ activemodel (= 7.0.4.1)
8
+ activesupport (= 7.0.4.1)
9
+ activesupport (7.0.4.1)
10
+ concurrent-ruby (~> 1.0, >= 1.0.2)
11
+ i18n (>= 1.6, < 2)
12
+ minitest (>= 5.1)
13
+ tzinfo (~> 2.0)
14
+ concurrent-ruby (1.1.10)
15
+ i18n (1.12.0)
16
+ concurrent-ruby (~> 1.0)
17
+ mini_portile2 (2.8.1)
18
+ minitest (5.17.0)
19
+ pg (1.4.5)
20
+ rake (13.0.6)
21
+ sqlite3 (1.6.0)
22
+ mini_portile2 (~> 2.8.0)
23
+ tzinfo (2.0.5)
24
+ concurrent-ruby (~> 1.0)
25
+
26
+ PLATFORMS
27
+ ruby
28
+
29
+ DEPENDENCIES
30
+ activerecord
31
+ pg
32
+ rake
33
+ sqlite3
34
+
35
+ BUNDLED WITH
36
+ 2.2.22
data/README.md CHANGED
@@ -1,8 +1,13 @@
1
1
  # Type Scopes
2
2
 
3
- Type scopes creates useful scopes based on the type of the columns of your models.
4
- 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
+ The goal is help to write eloquent code such as:
5
5
 
6
+ ```ruby
7
+ Transaction.paid_after(Date.yesterday).amount_between(100, 200).not_refunded
8
+ ```
9
+
10
+ It handles dates, times, strings, numerics and booleans.
6
11
  Here are examples for all the available scopes:
7
12
 
8
13
  ```ruby
@@ -10,7 +15,7 @@ Here are examples for all the available scopes:
10
15
  # amount: decimal
11
16
  # description: string
12
17
  class Transaction < ActiveRecord::Base
13
- include TypeScopes
18
+ TypeScopes.inject self
14
19
  end
15
20
 
16
21
  # Time scopes
@@ -35,8 +40,21 @@ Transaction.amount_not_within(100, 200) # => where("amount <= 100 OR amount >= 2
35
40
 
36
41
  # String scopes
37
42
  Transaction.description_contains("foo") # => where("description LIKE '%foo%'")
43
+ Transaction.description_contains("foo", sensitive: false) # => where("description ILIKE '%foo%'")
38
44
  Transaction.description_starts_with("foo") # => where("description LIKE 'foo%'")
45
+ Transaction.description_starts_with("foo", sensitive: false) # => where("description ILIKE 'foo%'")
46
+ Transaction.description_does_not_start_with("foo") # => where("description NOT LIKE 'foo%'")
47
+ Transaction.description_does_not_start_with("foo", sensitive: false) # => where("description NOT ILIKE 'foo%'")
39
48
  Transaction.description_ends_with("foo") # => where("description LIKE '%foo'")
49
+ Transaction.description_ends_with("foo", sensitive: false) # => where("description ILIKE '%foo'")
50
+ Transaction.description_does_not_end_with("foo") # => where("description NOT LIKE '%foo'")
51
+ Transaction.description_does_not_end_with("foo", sensitive: false) # => where("description NOT ILIKE '%foo'")
52
+ Transaction.description_like("%foo%") # => where("description LIKE '%foo%'")
53
+ Transaction.description_not_like("%foo%") # => where("description NOT LIKE '%foo%'")
54
+ Transaction.description_ilike("%foo%") # => where("description ILIKE '%foo%'")
55
+ Transaction.description_not_ilike("%foo%") # => where("description NOT ILIKE '%foo%'")
56
+ Transaction.description_matches("^Regex$") # => where("description ~ '^Regex$'")
57
+ Transaction.description_does_not_match("^Regex$") # => where("description !~ '^Regex$'")
40
58
 
41
59
  # Boolean scopes
42
60
  Transaction.non_profit # => where("non_profit = true")
@@ -49,7 +67,7 @@ Transaction.was_processed # => where("was_processed = true")
49
67
  Transaction.was_not_processed # => where("was_processed = false")
50
68
  ```
51
69
 
52
- For the string scope the pattern matching is escaped:
70
+ For the string colums, the pattern matching is escaped. So it's safe to provide directly a user input. There is an exception for the `column_like`, `column_ilike`, `column_matches` and `column_does_not_match` where the pattern is not escaped and you shouldn't provide untrusted strings.
53
71
 
54
72
  ```ruby
55
73
  Transaction.description_contains("%foo_") # => where("description LIKE '%[%]foo[_]%'")
@@ -57,26 +75,22 @@ Transaction.description_contains("%foo_") # => where("description LIKE '%[%]foo[
57
75
 
58
76
  ## Install
59
77
 
60
- Add to your Gemfile:
78
+ Add to your Gemfile `gem "type_scopes"` and run in your terminal `bundle install`.
79
+ Then call `TypeScopes.inject self` from your models:
61
80
 
62
81
  ```ruby
63
- gem "type_scopes"
64
- ```
65
-
66
- And run in your terminal:
82
+ # /app/models/transaction.rb
83
+ class Transaction < ApplicationRecord
84
+ # Creates scope for all supported column types
85
+ TypeScopes.inject self
67
86
 
68
- ```shell
69
- bundle install
70
- ```
71
-
72
- Then include TypeScopes from your models:
73
-
74
- ```ruby
75
- class Transaction < ActiveRecord::Base
76
- include TypeScopes
87
+ # Or if you prefer to enable scopes for specific columns only
88
+ TypeScopes.inject self, :amount, :paid_at
77
89
  end
78
90
  ```
79
91
 
92
+ In case there is a conflict with a scope name, TypeScopes won't over write your existing scope. You can safely inject TypeScopes and it won't break any scope defined previously.
93
+
80
94
  ## MIT License
81
95
 
82
96
  Made by [Base Secrète](https://basesecrete.com/en).
data/Rakefile ADDED
@@ -0,0 +1,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,40 @@
1
+ class TypeScopes::String < TypeScopes
2
+ def self.types
3
+ ["character", "text", "varchar"].freeze
4
+ end
5
+
6
+ def self.inject_for_column(model, name)
7
+ column = model.arel_table[name]
8
+ append_scope(model, :"#{name}_like", lambda { |str, sensitive: true| where(column.matches(str, nil, sensitive)) })
9
+ append_scope(model, :"#{name}_not_like", lambda { |str, sensitive: true| where(column.does_not_match(str, nil, sensitive)) })
10
+ append_scope(model, :"#{name}_ilike", lambda { |str| where(column.matches(str)) })
11
+ append_scope(model, :"#{name}_not_ilike", lambda { |str| where(column.does_not_match(str)) })
12
+
13
+ append_scope(model, :"#{name}_contains", lambda { |str, sensitive: true|
14
+ send("#{name}_like", "%#{sanitize_sql_like(str)}%", sensitive: sensitive)
15
+ })
16
+
17
+ append_scope(model, :"#{name}_does_not_contain", lambda { |str, sensitive: true|
18
+ send("#{name}_not_like", "%#{sanitize_sql_like(str)}%", sensitive: sensitive)
19
+ })
20
+
21
+ append_scope(model, :"#{name}_starts_with", lambda { |str, sensitive: true|
22
+ send("#{name}_like", "#{sanitize_sql_like(str)}%", sensitive: sensitive)
23
+ })
24
+
25
+ append_scope(model, :"#{name}_does_not_start_with", lambda { |str, sensitive: true|
26
+ send("#{name}_not_like", "#{sanitize_sql_like(str)}%", sensitive: sensitive)
27
+ })
28
+
29
+ append_scope(model, :"#{name}_ends_with", lambda { |str, sensitive: true|
30
+ send("#{name}_like", "%#{sanitize_sql_like(str)}", sensitive: sensitive)
31
+ })
32
+
33
+ append_scope(model, :"#{name}_does_not_end_with", lambda { |str, sensitive: true|
34
+ send("#{name}_not_like", "%#{sanitize_sql_like(str)}", sensitive: sensitive)
35
+ })
36
+
37
+ append_scope(model, :"#{name}_matches", lambda { |str, sensitive: true| where(column.matches_regexp(str, sensitive)) })
38
+ append_scope(model, :"#{name}_does_not_match", lambda { |str, sensitive: true| where(column.does_not_match_regexp(str, sensitive)) })
39
+ end
40
+ end
@@ -0,0 +1,23 @@
1
+ class TypeScopes::Time < TypeScopes
2
+ def self.types
3
+ ["timestamp", "datetime", "date"].freeze
4
+ end
5
+
6
+ def self.inject_for_column(model, name)
7
+ short_name = shorten_column_name(name)
8
+ column = model.arel_table[name]
9
+
10
+ append_scope(model, :"#{short_name}_to", lambda { |date| where(column.lteq(date)) })
11
+ append_scope(model, :"#{short_name}_from", lambda { |date| where(column.gteq(date)) })
12
+ append_scope(model, :"#{short_name}_after", lambda { |date| where(column.gt(date)) })
13
+ append_scope(model, :"#{short_name}_before", lambda { |date| where(column.lt(date)) })
14
+ append_scope(model, :"#{short_name}_between", lambda { |from, to| where(name => from..to) })
15
+ append_scope(model, :"#{short_name}_not_between", lambda { |from, to| where.not(name => from..to) })
16
+ append_scope(model, :"#{short_name}_within", lambda { |from, to| where(column.gt(from)).where(column.lt(to)) })
17
+ append_scope(model, :"#{short_name}_not_within", lambda { |from, to| where(column.lteq(from).or(column.gteq(to))) })
18
+ end
19
+
20
+ def self.shorten_column_name(name)
21
+ name.chomp("_at").chomp("_on")
22
+ end
23
+ end
@@ -1,3 +1,3 @@
1
- module TypeScopes
2
- VERSION = "0.4.0".freeze
1
+ class TypeScopes
2
+ VERSION = "0.6.1".freeze
3
3
  end
data/lib/type_scopes.rb CHANGED
@@ -1,13 +1,34 @@
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 = nil)
3
+ column_names ||= model.table_exists? ? model.columns.map(&:name) : []
4
+ for name in column_names
5
+ if column = model.columns_hash[name]
6
+ Time.support?(column.sql_type) && Time.inject_for_column(model, name)
7
+ String.support?(column.sql_type) && String.inject_for_column(model, name)
8
+ Numeric.support?(column.sql_type) && Numeric.inject_for_column(model, name)
9
+ Boolean.support?(column.sql_type) && Boolean.inject_for_column(model, name)
10
+ end
11
+ end
12
+ end
13
+
14
+ def self.append_scope(model, name, block)
15
+ model.scope(name, block) if !model.respond_to?(name, true)
16
+ end
17
+
18
+ def self.support?(column_type)
19
+ types.any? { |type| column_type.include?(type) }
20
+ end
21
+
22
+ def self.types
23
+ raise NotImplementedError
24
+ end
25
+
26
+ def self.inject_for_column(model, name)
27
+ raise NotImplementedError
12
28
  end
13
29
  end
30
+
31
+ require "type_scopes/time"
32
+ require "type_scopes/string"
33
+ require "type_scopes/numeric"
34
+ require "type_scopes/boolean"
@@ -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,39 @@
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.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alexis Bernard
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-06-22 00:00:00.000000000 Z
11
+ date: 2024-02-22 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
+ - CHANGELOG.md
22
+ - Gemfile
23
+ - Gemfile.lock
20
24
  - README.md
21
- - lib/boolean_scopes.rb
22
- - lib/numeric_scopes.rb
23
- - lib/string_scopes.rb
24
- - lib/timestamp_scopes.rb
25
+ - Rakefile
25
26
  - lib/type_scopes.rb
27
+ - lib/type_scopes/boolean.rb
28
+ - lib/type_scopes/numeric.rb
29
+ - lib/type_scopes/string.rb
30
+ - lib/type_scopes/time.rb
26
31
  - lib/type_scopes/version.rb
32
+ - test/test_helper.rb
33
+ - test/type_scopes/boolean_test.rb
34
+ - test/type_scopes/numeric_test.rb
35
+ - test/type_scopes/string_test.rb
36
+ - test/type_scopes/time_test.rb
27
37
  - type_scopes.gemspec
28
38
  homepage: https://github.com/BaseSecrete/type_scopes
29
39
  licenses:
@@ -44,8 +54,13 @@ required_rubygems_version: !ruby/object:Gem::Requirement
44
54
  - !ruby/object:Gem::Version
45
55
  version: '0'
46
56
  requirements: []
47
- rubygems_version: 3.0.3
57
+ rubygems_version: 3.2.22
48
58
  signing_key:
49
59
  specification_version: 4
50
- summary: Automatic scopes for ActiveRecord models.
51
- test_files: []
60
+ summary: Semantic scopes for your ActiveRecord models.
61
+ test_files:
62
+ - test/test_helper.rb
63
+ - test/type_scopes/boolean_test.rb
64
+ - test/type_scopes/numeric_test.rb
65
+ - test/type_scopes/string_test.rb
66
+ - test/type_scopes/time_test.rb
@@ -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