type_scopes 0.1.0 → 0.6.0

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