type_scopes 0.4.0 → 0.6.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml 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