type_scopes 0.1.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: e6f3c1b8e9ddeff61759d2244082f11058895dd1
4
- data.tar.gz: 388729c5dee7cd2e6fd6668e558618ad68a236b4
2
+ SHA256:
3
+ metadata.gz: d3ac3e90478e5e0aca44a37d9422d212af77ea01939072b12845d19f139ba3e2
4
+ data.tar.gz: 3fd3b0d8964ed751f3c304c91e0298ee4d4d8eb56a325ccb350193d4f92ea412
5
5
  SHA512:
6
- metadata.gz: 9abff35463947151c2ab63053c7fda13ee2107c08f4a979ef0354224a20ea0dd6686392e49ae0319ed053f5df7473b9717e6838ffe983e81a4835627d310e277
7
- data.tar.gz: 1dd119ba66eba838255ae0d7458e03459b7a493d01a489f2883d9ec093e9e9aa2651b10a878f4bd8f7fc53d72764702b5cb94a6da397cd5a549fa6cb9390a48a
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,24 +1,27 @@
1
1
  # Type Scopes
2
2
 
3
- Type scopes creates useful scopes based on the type of the columns of your models.
4
- It handles dates, times, strings and 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
- Here is an example of all the available scopes:
6
+ Here are examples for all the available scopes:
7
7
 
8
8
  ```ruby
9
9
  # paid_at: datetime
10
10
  # amount: decimal
11
11
  # description: string
12
12
  class Transaction < ActiveRecord::Base
13
- include TypeScopes
13
+ TypeScopes.inject self
14
14
  end
15
15
 
16
16
  # Time scopes
17
- Transaction.paid_to("2017-09-06") # => where("paid_to <= '2017-09-06'")
18
- Transaction.paid_from("2017-09-06") # => where("paid_to >= '2017-09-06'")
19
- Transaction.paid_after("2017-09-06") # => where("paid_to > '2017-09-06'")
20
- Transaction.paid_before("2017-09-06") #= where("paid_to < '2017-09-06'")
21
- Transaction.paid_between("2017-09-06", "2017-09-07") # => where("paid_to BETWEEN '2017-09-06' AND '2017-09-07'")
17
+ Transaction.paid_to("2017-09-06") # => where("paid_at <= '2017-09-06'")
18
+ Transaction.paid_from("2017-09-06") # => where("paid_at >= '2017-09-06'")
19
+ Transaction.paid_after("2017-09-06") # => where("paid_at > '2017-09-06'")
20
+ Transaction.paid_before("2017-09-06") #= where("paid_at < '2017-09-06'")
21
+ Transaction.paid_between("2017-09-06", "2017-09-07") # => where("paid_at BETWEEN '2017-09-06' AND '2017-09-07'")
22
+ Transaction.paid_not_between("2017-09-06", "2017-09-07") # => where("paid_at NOT BETWEEN '2017-09-06' AND '2017-09-07'")
23
+ Transaction.paid_within("2017-09-06", "2017-09-07") # => where("paid_at > '2017-09-06' AND paid_at < '2017-09-07'")
24
+ Transaction.paid_not_within("2017-09-06", "2017-09-07") # => where("paid_at <= '2017-09-06' OR paid_at >= '2017-09-07'")
22
25
 
23
26
  # Numeric scopes
24
27
  Transaction.amount_to(100) # => where("amount <= 100")
@@ -26,14 +29,40 @@ Transaction.amount_from(100) # => where("amount >= 100")
26
29
  Transaction.amount_above(100) # => where("amount > 100")
27
30
  Transaction.amount_below(100) # => where("amount < 100")
28
31
  Transaction.amount_between(100, 200) # => where("amount BETWEEN 100 AND 200")
32
+ Transaction.amount_not_between(100, 200) # => where("amount NOT BETWEEN 100 AND 200")
33
+ Transaction.amount_within(100, 200) # => where("amount > 100 AND amount < 200")
34
+ Transaction.amount_not_within(100, 200) # => where("amount <= 100 OR amount >= 200")
29
35
 
30
36
  # String scopes
31
37
  Transaction.description_contains("foo") # => where("description LIKE '%foo%'")
38
+ Transaction.description_contains("foo", sensitive: false) # => where("description ILIKE '%foo%'")
32
39
  Transaction.description_starts_with("foo") # => where("description LIKE 'foo%'")
40
+ Transaction.description_starts_with("foo", sensitive: false) # => where("description ILIKE 'foo%'")
41
+ Transaction.description_does_not_start_with("foo") # => where("description NOT LIKE 'foo%'")
42
+ Transaction.description_does_not_start_with("foo", sensitive: false) # => where("description NOT ILIKE 'foo%'")
33
43
  Transaction.description_ends_with("foo") # => where("description LIKE '%foo'")
44
+ Transaction.description_ends_with("foo", sensitive: false) # => where("description ILIKE '%foo'")
45
+ Transaction.description_does_not_end_with("foo") # => where("description NOT LIKE '%foo'")
46
+ Transaction.description_does_not_end_with("foo", sensitive: false) # => where("description NOT ILIKE '%foo'")
47
+ Transaction.description_like("%foo%") # => where("description LIKE '%foo%'")
48
+ Transaction.description_not_like("%foo%") # => where("description NOT LIKE '%foo%'")
49
+ Transaction.description_ilike("%foo%") # => where("description ILIKE '%foo%'")
50
+ Transaction.description_not_ilike("%foo%") # => where("description NOT ILIKE '%foo%'")
51
+ Transaction.description_matches("^Regex$") # => where("description ~ '^Regex$'")
52
+ Transaction.description_does_not_match("^Regex$") # => where("description !~ '^Regex$'")
53
+
54
+ # Boolean scopes
55
+ Transaction.non_profit # => where("non_profit = true")
56
+ Transaction.not_non_profit # => where("non_profit = false")
57
+ Transaction.is_valid # => where("is_valid = true")
58
+ Transaction.is_not_valid # => where("is_valid = false")
59
+ Transaction.has_payment # => where("has_payment = true")
60
+ Transaction.has_not_payment # => where("has_payment = false")
61
+ Transaction.was_processed # => where("was_processed = true")
62
+ Transaction.was_not_processed # => where("was_processed = false")
34
63
  ```
35
64
 
36
- For the string 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.
37
66
 
38
67
  ```ruby
39
68
  Transaction.description_contains("%foo_") # => where("description LIKE '%[%]foo[_]%'")
@@ -41,26 +70,21 @@ Transaction.description_contains("%foo_") # => where("description LIKE '%[%]foo[
41
70
 
42
71
  ## Install
43
72
 
44
- Add to your Gemfile:
73
+ Add to your Gemfile `gem "type_scopes"` and run in your terminal `bundle install`. Then call `TypeScopes.inject` from your models:
45
74
 
46
75
  ```ruby
47
- gem "type_scopes"
48
- ```
49
-
50
- And run in your terminal:
51
-
52
- ```shell
53
- bundle install
54
- ```
76
+ # /app/models/transaction.rb
77
+ class Transaction < ApplicationRecord
78
+ # Creates scope for all supported column types
79
+ TypeScopes.inject self
55
80
 
56
- Then include TypeScopes from your models:
57
-
58
- ```ruby
59
- class Transaction < ActiveRecord::Base
60
- include TypeScopes
81
+ # Or if you prefer to enable scopes for specific columns only
82
+ TypeScopes.inject self, :amount, :paid_at
61
83
  end
62
84
  ```
63
85
 
86
+ In case there is a conflict with a scope name, TypeScopes won't over write your existing scope. You can safely inject TypeScopes and it won't break any scope defined previously.
87
+
64
88
  ## MIT License
65
89
 
66
90
  Made by [Base Secrète](https://basesecrete.com/en).
data/Rakefile ADDED
@@ -0,0 +1,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.1.0".freeze
1
+ class TypeScopes
2
+ VERSION = "0.6.0".freeze
3
3
  end
data/lib/type_scopes.rb CHANGED
@@ -1,11 +1,33 @@
1
- require "string_scopes"
2
- require "numeric_scopes"
3
- require "timestamp_scopes"
1
+ class TypeScopes
2
+ def self.inject(model, column_names = model.columns.map(&:name))
3
+ for name in column_names
4
+ if column = model.columns_hash[name]
5
+ Time.support?(column.sql_type) && Time.inject_for_column(model, name)
6
+ String.support?(column.sql_type) && String.inject_for_column(model, name)
7
+ Numeric.support?(column.sql_type) && Numeric.inject_for_column(model, name)
8
+ Boolean.support?(column.sql_type) && Boolean.inject_for_column(model, name)
9
+ end
10
+ end
11
+ end
12
+
13
+ def self.append_scope(model, name, block)
14
+ model.scope(name, block) if !model.respond_to?(name, true)
15
+ end
16
+
17
+ def self.support?(column_type)
18
+ types.any? { |type| column_type.include?(type) }
19
+ end
20
+
21
+ def self.types
22
+ raise NotImplementedError
23
+ end
4
24
 
5
- module TypeScopes
6
- def self.included(model)
7
- model.include(StringScopes)
8
- model.include(NumericScopes)
9
- model.include(TimestampScopes)
25
+ def self.inject_for_column(model, name)
26
+ raise NotImplementedError
10
27
  end
11
28
  end
29
+
30
+ require "type_scopes/time"
31
+ require "type_scopes/string"
32
+ require "type_scopes/numeric"
33
+ require "type_scopes/boolean"
@@ -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,28 +1,38 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: type_scopes
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.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: 2017-09-06 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/numeric_scopes.rb
22
- - lib/string_scopes.rb
23
- - lib/timestamp_scopes.rb
24
+ - Rakefile
24
25
  - lib/type_scopes.rb
26
+ - lib/type_scopes/boolean.rb
27
+ - lib/type_scopes/numeric.rb
28
+ - lib/type_scopes/string.rb
29
+ - lib/type_scopes/time.rb
25
30
  - lib/type_scopes/version.rb
31
+ - test/test_helper.rb
32
+ - test/type_scopes/boolean_test.rb
33
+ - test/type_scopes/numeric_test.rb
34
+ - test/type_scopes/string_test.rb
35
+ - test/type_scopes/time_test.rb
26
36
  - type_scopes.gemspec
27
37
  homepage: https://github.com/BaseSecrete/type_scopes
28
38
  licenses:
@@ -43,10 +53,13 @@ required_rubygems_version: !ruby/object:Gem::Requirement
43
53
  - !ruby/object:Gem::Version
44
54
  version: '0'
45
55
  requirements: []
46
- rubyforge_project:
47
- rubygems_version: 2.4.8
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: []
52
- has_rdoc:
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,24 +0,0 @@
1
- module NumericScopes
2
- def self.included(model)
3
- model.extend(ClassMethods)
4
- model.create_numeric_scopes
5
- end
6
-
7
- module ClassMethods
8
- def create_numeric_scopes
9
- for column in columns
10
- if column.sql_type.index("integer") == 0
11
- create_numeric_scopes_for_column(column.name)
12
- end
13
- end
14
- end
15
-
16
- def create_numeric_scopes_for_column(name)
17
- scope :"#{name}_to", lambda { |value| where("#{quoted_table_name}.#{name} <= ?", value) }
18
- scope :"#{name}_from", lambda { |value| where("#{quoted_table_name}.#{name} >= ?", value) }
19
- scope :"#{name}_above", lambda { |value| where("#{quoted_table_name}.#{name} > ?", value) }
20
- scope :"#{name}_below", lambda { |value| where("#{quoted_table_name}.#{name} < ?", value) }
21
- scope :"#{name}_between", lambda { |from, to| where("#{quoted_table_name}.#{name} BETWEEN ? AND ?", from, to) }
22
- end
23
- end
24
- end
data/lib/string_scopes.rb DELETED
@@ -1,28 +0,0 @@
1
- module StringScopes
2
- def self.included(model)
3
- model.extend(ClassMethods)
4
- model.create_string_scopes
5
- end
6
-
7
- def self.escape(string)
8
- string = string.gsub("%".freeze, "[%]".freeze)
9
- string.gsub!("_".freeze, "[_]".freeze)
10
- string
11
- end
12
-
13
- module ClassMethods
14
- def create_string_scopes
15
- for column in columns
16
- if column.sql_type.index("character") == 0 || column.sql_type.index("text") == 0
17
- create_string_scopes_for_column(column.name)
18
- end
19
- end
20
- end
21
-
22
- def create_string_scopes_for_column(name)
23
- scope :"#{name}_contains", lambda { |str| where("#{quoted_table_name}.#{name} LIKE ?", "%#{StringScopes.escape(str)}%") }
24
- scope :"#{name}_starts_with", lambda { |str| where("#{quoted_table_name}.#{name} LIKE ?", "#{StringScopes.escape(str)}%") }
25
- scope :"#{name}_ends_with", lambda { |str| where("#{quoted_table_name}.#{name} LIKE ?", "%#{StringScopes.escape(str)}") }
26
- end
27
- end
28
- end
@@ -1,29 +0,0 @@
1
- module TimestampScopes
2
- def self.included(model)
3
- model.extend(ClassMethods)
4
- model.create_timestamp_scopes
5
- end
6
-
7
- module ClassMethods
8
- def create_timestamp_scopes
9
- for column in columns
10
- if column.sql_type.index("timestamp") == 0
11
- create_timestamp_scopes_for_column(column.name)
12
- end
13
- end
14
- end
15
-
16
- def create_timestamp_scopes_for_column(name)
17
- short_name = shorten_column_name(name)
18
- scope :"#{short_name}_to", lambda { |date| where("#{quoted_table_name}.#{name} <= ?", date) }
19
- scope :"#{short_name}_from", lambda { |date| where("#{quoted_table_name}.#{name} >= ?", date) }
20
- scope :"#{short_name}_after", lambda { |date| where("#{quoted_table_name}.#{name} > ?", date) }
21
- scope :"#{short_name}_before", lambda { |date| where("#{quoted_table_name}.#{name} < ?", date) }
22
- scope :"#{short_name}_between", lambda { |from, to| where("#{quoted_table_name}.#{name} BETWEEN ? AND ?", from, to) }
23
- end
24
-
25
- def shorten_column_name(name)
26
- name.chomp("_at").chomp("_on")
27
- end
28
- end
29
- end