yiffspace-ext 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 4fc882fca40b80202278d32120a9431217eec12f12ecc87b3c6f822ed207beb1
4
+ data.tar.gz: 52aec8e7846120c2130d2a215fdb1ab9b20ab149f1dc91d8ee8daaaf16da493b
5
+ SHA512:
6
+ metadata.gz: 26b0192994a1d69ed566098e9f25decc3ce9ac519c9b68133cb3eabad8a069b16ef7bf9f5f30c4a3db2911ce118f2a75b859d08a785c1f7286127b5d685e0d73
7
+ data.tar.gz: ac8f6ad4ff4a3291b1430de0314862f196dfab5771a812b5baab48fa8b2560299860612eed89c8216eea265208dfd0179f8b95c04b31723b0793a1ee33312a2d
data/CHANGELOG.md ADDED
@@ -0,0 +1,6 @@
1
+ ## 0.0.1
2
+
3
+ - Initial release, extracted from the `yiffspace` gem's generic
4
+ `YiffSpace::Extensions::{Object,Hash,Enumerable,String}` and
5
+ `YiffSpace::Extensions::ActiveRecord::WhereChain`, plus their `core_ext`
6
+ global aliases.
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright Donovan_DMC
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,40 @@
1
+ # YiffSpace::Ext
2
+
3
+ Generic Ruby/ActiveRecord core extensions - `Object#to_b`/`#truthy?`/`#falsy?`,
4
+ `Hash#to_open_hash`, `Enumerable#parallel`, `String#to_escaped_for_sql_like`,
5
+ and the `.where` chain DSL (`.like`/`.ilike`/`.has_bits`/`.regex`/etc) - for
6
+ https://yiff.space and related projects. No dependency on `yiffspace` itself.
7
+
8
+ ## Installation
9
+
10
+ Add this line to your application's Gemfile:
11
+
12
+ ```ruby
13
+ gem "yiffspace-ext"
14
+ ```
15
+
16
+ And then execute:
17
+
18
+ ```bash
19
+ $ bundle install
20
+ ```
21
+
22
+ ## Usage
23
+
24
+ The namespaced extensions live under `YiffSpace::Extensions::*` and are
25
+ autoloaded. The old global monkeypatch style is opt-in:
26
+
27
+ ```ruby
28
+ require "yiffspace/core_ext/all" # or object/all, hash/all, etc individually
29
+ ```
30
+
31
+ `core_ext/all` also picks up `yiffspace-arel`'s lateral-join/unnest globals
32
+ if that gem happens to be installed too.
33
+
34
+ ## Contributing
35
+
36
+ Go away
37
+
38
+ ## License
39
+
40
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require("bundler/gem_tasks")
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require("active_record/relation")
4
+ require_relative("../../extensions/active_record/where_chain")
5
+
6
+ ActiveRecord::QueryMethods::WhereChain.include(YiffSpace::Extensions::ActiveRecord::WhereChain)
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative("active_record/where_chain")
4
+ require_relative("enumerable/all")
5
+ require_relative("hash/all")
6
+ require_relative("object/all")
7
+ require_relative("string/all")
8
+
9
+ # yiffspace-arel's lateral-join/unnest core_ext, only if that gem happens to be installed too.
10
+ begin
11
+ require("yiffspace/core_ext/active_record/all")
12
+ rescue LoadError
13
+ nil
14
+ end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative("parallel")
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative("../../extensions/enumerable/parallel")
4
+
5
+ module Enumerable
6
+ include(YiffSpace::Extensions::Enumerable::Parallel)
7
+ end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative("to_open_hash")
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative("../../extensions/hash/to_open_hash")
4
+
5
+ class Hash
6
+ include(YiffSpace::Extensions::Hash::ToOpenHash)
7
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative("to_b")
4
+ require_relative("truthy_falsy")
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative("../../extensions/object/to_b")
4
+
5
+ class Object
6
+ include(YiffSpace::Extensions::Object::ToB)
7
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative("../../extensions/object/truthy_falsy")
4
+
5
+ class Object
6
+ include(YiffSpace::Extensions::Object::TruthyFalsy)
7
+ end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative("sql")
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative("../../extensions/string/sql")
4
+
5
+ class String
6
+ include(YiffSpace::Extensions::String::Sql)
7
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YiffSpace
4
+ module Ext
5
+ VERSION = "0.0.1"
6
+ end
7
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require("zeitwerk")
4
+
5
+ module YiffSpace
6
+ module Ext
7
+ end
8
+ end
9
+
10
+ loader = Zeitwerk::Loader.for_gem_extension(YiffSpace)
11
+ loader.ignore("#{__dir__}/core_ext")
12
+ loader.setup
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YiffSpace
4
+ module Extensions
5
+ module ActiveRecord
6
+ module WhereChain
7
+ %i[
8
+ lt lteq gt gteq
9
+ between not_between in not_in like
10
+ ilike not_like not_ilike
11
+ like_all ilike_all not_like_all not_ilike_all
12
+ like_any ilike_any not_like_any not_ilike_any
13
+ has_bits not_has_bits
14
+ ].each do |method|
15
+ define_method(method) do |conditions|
16
+ build(conditions, method)
17
+ end
18
+ end
19
+
20
+ alias lte lteq
21
+ alias gte gteq
22
+
23
+ # https://www.postgresql.org/docs/current/static/functions-matching.html#FUNCTIONS-POSIX-REGEXP
24
+ # "(?e)" means force use of ERE syntax; see sections 9.7.3.1 and 9.7.3.4.
25
+ def regex(conditions = {}, flags: "e", **kwargs)
26
+ conditions.merge!(kwargs) if conditions.is_a?(::Hash)
27
+ build(conditions, :regex, flags)
28
+ end
29
+
30
+ def not_regex(conditions = {}, flags: "e", **kwargs)
31
+ conditions.merge!(kwargs) if conditions.is_a?(::Hash)
32
+ build(conditions, :not_regex, flags)
33
+ end
34
+
35
+ def tsquery(conditions = {}, ts_config: "english", **kwargs)
36
+ conditions.merge!(kwargs) if conditions.is_a?(::Hash)
37
+ build(conditions, :tsquery, ts_config)
38
+ end
39
+
40
+ METHODS = {
41
+ lt: ->(table_name, column, value) { ::Arel::Table.new(table_name)[column].lt(value) },
42
+ lteq: ->(table_name, column, value) { ::Arel::Table.new(table_name)[column].lteq(value) },
43
+ gt: ->(table_name, column, value) { ::Arel::Table.new(table_name)[column].gt(value) },
44
+ gteq: ->(table_name, column, value) { ::Arel::Table.new(table_name)[column].gteq(value) },
45
+ between: ->(table_name, column, value) { ::Arel::Table.new(table_name)[column].between(value) },
46
+ not_between: ->(table_name, column, value) { ::Arel::Table.new(table_name)[column].not_between(value) },
47
+ in: ->(table_name, column, value) { ::Arel::Table.new(table_name)[column].in(value) },
48
+ not_in: ->(table_name, column, value) { ::Arel::Table.new(table_name)[column].not_in(value) },
49
+ like: ->(table_name, column, value) { ::Arel::Table.new(table_name)[column].matches(value.to_escaped_for_sql_like, ::Arel.sql("E'\\\\'"), true) },
50
+ ilike: ->(table_name, column, value) { ::Arel::Table.new(table_name)[column].matches(value.to_escaped_for_sql_like, ::Arel.sql("E'\\\\'"), false) },
51
+ not_like: ->(table_name, column, value) { ::Arel::Table.new(table_name)[column].does_not_match(value.to_escaped_for_sql_like, ::Arel.sql("E'\\\\'"), true) },
52
+ not_ilike: ->(table_name, column, value) { ::Arel::Table.new(table_name)[column].does_not_match(value.to_escaped_for_sql_like, ::Arel.sql("E'\\\\'"), false) },
53
+ like_all: ->(table_name, column, value) { ::Arel::Table.new(table_name)[column].matches_all(value.map(&:to_escaped_for_sql_like), ::Arel.sql("E'\\\\'"), true) },
54
+ ilike_all: ->(table_name, column, value) { ::Arel::Table.new(table_name)[column].matches_all(value.map(&:to_escaped_for_sql_like), ::Arel.sql("E'\\\\'"), false) },
55
+ not_like_all: ->(table_name, column, value) { ::Arel::Table.new(table_name)[column].does_not_match_all(value.map(&:to_escaped_for_sql_like), ::Arel.sql("E'\\\\'"), true) },
56
+ not_ilike_all: ->(table_name, column, value) { ::Arel::Table.new(table_name)[column].does_not_match_all(value.map(&:to_escaped_for_sql_like), ::Arel.sql("E'\\\\'"), false) },
57
+ like_any: ->(table_name, column, value) { ::Arel::Table.new(table_name)[column].matches_any(value.map(&:to_escaped_for_sql_like), ::Arel.sql("E'\\\\'"), true) },
58
+ ilike_any: ->(table_name, column, value) { ::Arel::Table.new(table_name)[column].matches_any(value.map(&:to_escaped_for_sql_like), ::Arel.sql("E'\\\\'"), false) },
59
+ not_like_any: ->(table_name, column, value) { ::Arel::Table.new(table_name)[column].does_not_match_any(value.map(&:to_escaped_for_sql_like), ::Arel.sql("E'\\\\'"), true) },
60
+ not_ilike_any: ->(table_name, column, value) { ::Arel::Table.new(table_name)[column].does_not_match_any(value.map(&:to_escaped_for_sql_like), ::Arel.sql("E'\\\\'"), false) },
61
+ regex: ->(table_name, column, value, flags) { ::Arel::Table.new(table_name)[column].matches_regexp("(?#{flags})#{value.is_a?(Regexp) ? value.source : value}") },
62
+ not_regex: ->(table_name, column, value, flags) { ::Arel::Table.new(table_name)[column].does_not_match_regexp("(?#{flags})#{value.is_a?(Regexp) ? value.source : value}") },
63
+ tsquery: ->(table_name, column, value, ts_config) { ::Arel.sql("to_tsvector(:ts_config, :table) @@ plainto_tsquery(:ts_config, :value)", ts_config: ts_config, value: value, table: ::Arel.sql("#{table_name}.#{column}")) },
64
+ has_bits: ->(table_name, column, value) { ::Arel.sql("(#{table_name}.#{column} & #{::Arel::Nodes.build_quoted(value).to_sql}) = #{::Arel::Nodes.build_quoted(value).to_sql}") },
65
+ not_has_bits: ->(table_name, column, value) { ::Arel.sql("(#{table_name}.#{column} & #{::Arel::Nodes.build_quoted(value).to_sql}) != #{::Arel::Nodes.build_quoted(value).to_sql}") },
66
+ }.freeze
67
+
68
+ private
69
+
70
+ def build(conditions, method, *extra)
71
+ model = @scope.klass
72
+
73
+ parsed_conditions = normalize_conditions(conditions, model)
74
+
75
+ arel_conditions = parsed_conditions.map do |(table_name, column), value|
76
+ METHODS.fetch(method).call(table_name, column, value, *extra)
77
+ end.reduce(:and)
78
+
79
+ @scope.where(arel_conditions)
80
+ end
81
+
82
+ def normalize_conditions(conditions, model, table_name = nil)
83
+ pairs = []
84
+
85
+ conditions.each do |key, value|
86
+ case key
87
+ when ::String, ::Symbol
88
+ key = key.to_s
89
+ if value.is_a?(::Hash)
90
+ pairs.concat(normalize_conditions(value, model, key))
91
+ elsif key.include?(".")
92
+ table, col = key.split(".", 2)
93
+ pairs << [[table, col], value]
94
+ else
95
+ pairs << [[table_name || model.table_name, key], value]
96
+ end
97
+ else
98
+ raise(ArgumentError, "Unsupported key type: #{key.class}")
99
+ end
100
+ end
101
+
102
+ pairs
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require("active_support/core_ext/module/attribute_accessors")
4
+
5
+ module YiffSpace
6
+ module Extensions
7
+ module Enumerable
8
+ module Parallel
9
+ mattr_accessor(:usable)
10
+ # Like `#each`, but perform the block on each item in parallel. Note that items aren't processed in order, so things
11
+ # like `parallel_each.map` that rely on ordering won't work.
12
+ def parallel_each(executor = :io, &)
13
+ return enum_for(:parallel_each, executor) unless block_given?
14
+
15
+ parallel_map(executor, &)
16
+ self
17
+ end
18
+
19
+ # Like `#map`, but in parallel.
20
+ def parallel_map(executor = :io, &)
21
+ return enum_for(:parallel_map, executor) unless block_given?
22
+
23
+ promises = map do |item|
24
+ Concurrent::Promises.future_on(executor, item, &)
25
+ end
26
+
27
+ Concurrent::Promises.zip_futures_on(executor, *promises).value!
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+
34
+ begin
35
+ require("concurrent-ruby")
36
+ YiffSpace::Extensions::Enumerable::Parallel.usable = true
37
+ rescue LoadError
38
+ YiffSpace::Extensions::Enumerable::Parallel.usable = false
39
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YiffSpace
4
+ module Extensions
5
+ module Hash
6
+ module ToOpenHash
7
+ def to_open_hash
8
+ ::YiffSpace::Utils::OpenHash.from(self)
9
+ end
10
+
11
+ alias with_open_access to_open_hash
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YiffSpace
4
+ module Extensions
5
+ module Object
6
+ module ToB
7
+ def to_b
8
+ case self
9
+ when ::String
10
+ !match?(/\A(false|f|no|n|off|0)\z/i)
11
+ when ::TrueClass
12
+ true
13
+ when ::FalseClass
14
+ false
15
+ else
16
+ to_s.to_b
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YiffSpace
4
+ module Extensions
5
+ module Object
6
+ module TruthyFalsy
7
+ def truthy?
8
+ case self
9
+ when ::String
10
+ match?(/\A(true|t|yes|y|on|1)\z/i)
11
+ when ::TrueClass
12
+ true
13
+ when ::FalseClass
14
+ false
15
+ else
16
+ to_s.truthy?
17
+ end
18
+ end
19
+
20
+ def falsy?
21
+ case self
22
+ when ::String
23
+ match?(/\A(false|f|no|n|off|0)\z/i)
24
+ when ::TrueClass
25
+ false
26
+ when ::FalseClass
27
+ true
28
+ else
29
+ to_s.falsy?
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YiffSpace
4
+ module Extensions
5
+ module String
6
+ module Sql
7
+ def to_escaped_for_sql_like
8
+ gsub(/%|_|\*|\\\*|\\\\|\\/) do |str|
9
+ case str
10
+ when "%" then '\%'
11
+ when "_" then '\_'
12
+ when "*" then "%"
13
+ when '\*' then "*"
14
+ when "\\\\", "\\" then "\\\\"
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
metadata ADDED
@@ -0,0 +1,112 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: yiffspace-ext
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Donovan_DMC
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 2026-09-04 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: activerecord
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '7.1'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '7.1'
26
+ - !ruby/object:Gem::Dependency
27
+ name: activesupport
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '7.1'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '7.1'
40
+ - !ruby/object:Gem::Dependency
41
+ name: zeitwerk
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '2.6'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '2.6'
54
+ description: Generic Ruby/ActiveRecord core extensions (Object/Hash/Enumerable/String,
55
+ WhereChain), for https://yiff.space and related projects
56
+ email:
57
+ - hewwo@yiff.rocks
58
+ executables: []
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - CHANGELOG.md
63
+ - LICENSE
64
+ - README.md
65
+ - Rakefile
66
+ - lib/yiffspace/core_ext/active_record/where_chain.rb
67
+ - lib/yiffspace/core_ext/all.rb
68
+ - lib/yiffspace/core_ext/enumerable/all.rb
69
+ - lib/yiffspace/core_ext/enumerable/parallel.rb
70
+ - lib/yiffspace/core_ext/hash/all.rb
71
+ - lib/yiffspace/core_ext/hash/to_open_hash.rb
72
+ - lib/yiffspace/core_ext/object/all.rb
73
+ - lib/yiffspace/core_ext/object/to_b.rb
74
+ - lib/yiffspace/core_ext/object/truthy_falsy.rb
75
+ - lib/yiffspace/core_ext/string/all.rb
76
+ - lib/yiffspace/core_ext/string/sql.rb
77
+ - lib/yiffspace/ext.rb
78
+ - lib/yiffspace/ext/version.rb
79
+ - lib/yiffspace/extensions/active_record/where_chain.rb
80
+ - lib/yiffspace/extensions/enumerable/parallel.rb
81
+ - lib/yiffspace/extensions/hash/to_open_hash.rb
82
+ - lib/yiffspace/extensions/object/to_b.rb
83
+ - lib/yiffspace/extensions/object/truthy_falsy.rb
84
+ - lib/yiffspace/extensions/string/sql.rb
85
+ homepage: https://yiff.space
86
+ licenses:
87
+ - MIT
88
+ metadata:
89
+ allowed_push_host: https://rubygems.org
90
+ homepage_uri: https://yiff.space
91
+ source_code_uri: https://github.com/YiffSpace/Gem/tree/master/yiffspace-ext
92
+ changelog_uri: https://github.com/YiffSpace/Gem/blob/master/yiffspace-ext/CHANGELOG.md
93
+ rubygems_mfa_required: 'true'
94
+ rdoc_options: []
95
+ require_paths:
96
+ - lib
97
+ required_ruby_version: !ruby/object:Gem::Requirement
98
+ requirements:
99
+ - - ">="
100
+ - !ruby/object:Gem::Version
101
+ version: 3.4.1
102
+ required_rubygems_version: !ruby/object:Gem::Requirement
103
+ requirements:
104
+ - - ">="
105
+ - !ruby/object:Gem::Version
106
+ version: '0'
107
+ requirements: []
108
+ rubygems_version: 3.6.2
109
+ specification_version: 4
110
+ summary: Generic Ruby/ActiveRecord core extensions (Object/Hash/Enumerable/String,
111
+ WhereChain), for https://yiff.space and related projects
112
+ test_files: []