yiffspace-search 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: 4921b1eca09fb7273db303f993933ad0fa3d8e133f6df4e173ddcdf7f39110e3
4
+ data.tar.gz: e7243afed8de644ff960bf7b7cbbc1dbe7409cff75b83287a371549de0221663
5
+ SHA512:
6
+ metadata.gz: a71fab7d37d718372b4ff94719d88247d53ca88d54f26881948f73452a660034c2210ae247d98206b0e3404166c884cbc8dd0da9303b1124704d11807a2bbcc2
7
+ data.tar.gz: 9cf9638a9010c220204ef0abdfb1731545ceead28cb0ebb35604b3388b9fe3b56564f7a24dd61cffd0c2af18bee20c7ce9f2299a887107c8200fee7008f45d52
data/CHANGELOG.md ADDED
@@ -0,0 +1,3 @@
1
+ ## 0.0.1
2
+
3
+ - Initial release, extracted from the `yiffspace` gem's `YiffSpace::Search::QueryBuilder`/`QueryDSL`/`QueryHelper`.
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,28 @@
1
+ # YiffSpace::Search
2
+
3
+ Query-param search DSL (`YiffSpace::Search::QueryDSL`/`QueryBuilder`/
4
+ `QueryHelper`), for https://yiff.space and related projects. Depends on
5
+ `yiffspace-core` (attribute matchers), `yiffspace-ext` (`.where.like`/
6
+ `.ilike`), and `yiffspace-user` (`user_class`).
7
+
8
+ ## Installation
9
+
10
+ Add this line to your application's Gemfile:
11
+
12
+ ```ruby
13
+ gem "yiffspace-search"
14
+ ```
15
+
16
+ And then execute:
17
+
18
+ ```bash
19
+ $ bundle install
20
+ ```
21
+
22
+ ## Contributing
23
+
24
+ Go away
25
+
26
+ ## License
27
+
28
+ 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,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require("bundler/gem_tasks")
4
+
5
+ # CI releases from an already-tagged, already-pushed commit checked out at a detached HEAD, so
6
+ # the default release task's git tag/push step has nothing to do and fails trying to push a
7
+ # branch ref that does not exist in that checkout.
8
+ Rake::Task["release"].clear
9
+ task("release" => "release:rubygem_push")
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YiffSpace
4
+ module Search
5
+ class QueryBuilder
6
+ attr_reader(:dsl, :klass, :relation, :user, :q)
7
+
8
+ # [param, db_field, type]
9
+ def initialize(dsl, klass, relation, user)
10
+ dsl ||= []
11
+ @dsl = dsl
12
+ @klass = klass
13
+ @relation = relation
14
+ @user = user
15
+ @q = relation
16
+ end
17
+
18
+ def process_dsl(dsl, params)
19
+ dsl[:fields].each do |param, field, type = nil, block = nil, options = {}|
20
+ value = params[param]
21
+ multi = options.fetch(:multi, nil) || false
22
+ like = options.fetch(:like, nil) || false
23
+ ilike = options.fetch(:ilike, nil) || false
24
+ normalize = options.fetch(:normalize, nil) || ->(value) { value }
25
+ next if value.nil?
26
+
27
+ value = normalize.call(value)
28
+ @q = block.call(q) if block
29
+ if type.is_a?(Proc)
30
+ args = [q, value, user, params]
31
+ @q = type.call(*args.first(type.arity))
32
+ next
33
+ end
34
+ field ||= param
35
+ if like
36
+ @q = q.where.like(field => value)
37
+ next
38
+ elsif ilike
39
+ @q = q.where.ilike(field => value)
40
+ next
41
+ end
42
+ type ||= QueryHelper.get_column(field, klass.table_name).sql_type_metadata.type
43
+ case type
44
+ when :boolean
45
+ @q = q.boolean_attribute_matches(field, value)
46
+ when :integer
47
+ # multiple comma separated values are implicitly supported
48
+ # trimming them seems unneeded
49
+ # value = value[0.. value.index(",") - 1] unless multi
50
+ @q = q.numeric_attribute_matches(field, value)
51
+ when :datetime
52
+ @q = q.datetime_attribute_matches(field, value)
53
+ when :text, :string
54
+ value = value.split(",").first(YiffSpace.config.max_multi_count.call) if multi # explicitly supports arrays
55
+ @q = q.text_attribute_matches(field, value)
56
+ when :inet
57
+ @q = q.ip_attribute_matches(field, value)
58
+ when :present
59
+ @q = q.attribute_present(field, value)
60
+ else
61
+ raise(ArgumentError, "Unknown type: #{type} for field: #{field}")
62
+ end
63
+ end
64
+
65
+ dsl[:associations].each do |attribute, klass, nested_dsl, join|
66
+ next if params[attribute].nil?
67
+
68
+ @q = q.joins(join)
69
+ nested_relation = klass.all
70
+ nested_builder = QueryBuilder.new(nested_dsl, klass, nested_relation, user)
71
+ nested_relation = nested_builder.search(params[attribute])
72
+ @q = q.merge(nested_relation)
73
+ end
74
+ end
75
+
76
+ def search(params = {})
77
+ params.transform_keys!(&:to_sym)
78
+ process_dsl(dsl, params)
79
+ q
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YiffSpace
4
+ module Search
5
+ class QueryDSL
6
+ attr_accessor(:relation, :fields, :associations, :no_id, :no_dates)
7
+
8
+ # [param, field, type/proc, Proc(relation)]
9
+ def initialize(relation)
10
+ @relation = relation
11
+ @fields = []
12
+ @associations = []
13
+ @no_id = false
14
+ @no_dates = false
15
+ end
16
+
17
+ def custom(param, proc, not: false, multi: false, &block)
18
+ @fields << [param, nil, proc, block, { not: binding.local_variable_get(:not), multi: multi }]
19
+ self
20
+ end
21
+
22
+ def field(param, db_field = param, type = nil, not: false, multi: false, wildcard: false, like: false, ilike: false, normalize: nil, &block)
23
+ type ||= QueryHelper.get_column(db_field, relation.table_name).sql_type_metadata.type
24
+ @fields << [param, db_field, type, block, { not: binding.local_variable_get(:not), multi: multi, wildcard: wildcard, like: like, ilike: ilike, normalize: normalize }]
25
+ self
26
+ end
27
+
28
+ def user(param, association = nil, not: false, &)
29
+ if param.is_a?(Array)
30
+ raise(ArgumentError, "You must specify an association when passing an array of parameters") if association.nil?
31
+ else
32
+ association ||= param
33
+ param = [:"#{param}_id", :"#{param}_name"]
34
+ end
35
+
36
+ case association
37
+ when Symbol, String
38
+ association = association.to_sym
39
+ associations = relation.reflect_on_all_associations(:belongs_to).map(&:name)
40
+ if associations.include?(association)
41
+ column = association_column(association)
42
+ else
43
+ column = association
44
+ end
45
+ when Array
46
+ column = association.first
47
+ association = association.second
48
+ end
49
+ field(param.first, column, not: binding.local_variable_get(:not), &) if param.first
50
+ custom(param.second, ->(q, v) { q.user_name_matches(association, v) }, not: binding.local_variable_get(:not), &) if param.second
51
+ self
52
+ end
53
+
54
+ def present(param, db_field = param, &)
55
+ field(param, db_field, :present, &)
56
+ end
57
+
58
+ # [attribute, class, dsl, join]
59
+ def association(*args, **kwargs)
60
+ if args.any?
61
+ attribute = args.first
62
+ as = args.second || attribute
63
+ ref = relation.reflect_on_association(attribute)
64
+ raise("Association #{attribute} does not exist on #{relation.name}") if ref.nil?
65
+
66
+ klass = ref.klass
67
+ dsl = klass.query_dsl.build
68
+ @associations << [as, klass, dsl, attribute]
69
+ user_class = YiffSpace.config.user_class
70
+ user(as, attribute) if klass == user_class && @fields.none? { |f| f.second == association_column(attribute) }
71
+ elsif kwargs.any?
72
+ attribute = kwargs.delete(:as)
73
+ to_array = lambda { |h|
74
+ k, v = h.first
75
+ [k, v.is_a?(Hash) ? to_array.call(v) : v].flatten
76
+ }
77
+ through = to_array.call(kwargs)
78
+ attribute ||= through.last
79
+ klass = relation
80
+ through.each { |k| klass = klass.reflect_on_association(k).klass }
81
+ dsl = klass.query_dsl.build
82
+ join = through.reverse.reduce { |acc, key| { key => acc } }
83
+ @associations << [attribute, klass, dsl, join]
84
+ else
85
+ raise(ArgumentError, "Missing association")
86
+ end
87
+ self
88
+ end
89
+
90
+ def no_id!
91
+ @no_id = true
92
+ @fields.reject! { |field| field.first == :id }
93
+ self
94
+ end
95
+
96
+ def no_dates!
97
+ @no_dates = true
98
+ @fields.reject! { |field| %i[created_at updated_at].include?(field.first) }
99
+ self
100
+ end
101
+
102
+ def build
103
+ @fields << %i[id id integer] if @fields.none? { |field| field.first == :id }
104
+ @fields << %i[created_at created_at datetime] if @fields.none? { |field| field.first == :created_at } && relation.attribute_names.include?("created_at")
105
+ @fields << %i[updated_at updated_at datetime] if @fields.none? { |field| field.first == :updated_at } && relation.attribute_names.include?("updated_at")
106
+ {
107
+ fields: fields,
108
+ associations: associations,
109
+ }
110
+ end
111
+
112
+ private
113
+
114
+ def association_column(association)
115
+ :"#{relation.table_name}.#{relation.reflect_on_association(association).foreign_key}"
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YiffSpace
4
+ module Search
5
+ module QueryHelper
6
+ module_function
7
+
8
+ def parse_conditions(conditions, model, table_name = nil)
9
+ pairs = []
10
+
11
+ conditions.each do |key, value|
12
+ case key
13
+ when String, Symbol
14
+ key = key.to_s
15
+ if value.is_a?(Hash)
16
+ pairs.concat(parse_conditions(value, model, key))
17
+ elsif key.include?(".")
18
+ table, col = key.split(".", 2)
19
+ pairs << [[table, col], value]
20
+ else
21
+ pairs << [[table_name || model.table_name, key], value]
22
+ end
23
+ else
24
+ raise(ArgumentError, "Unsupported key type: #{key.class}")
25
+ end
26
+ end
27
+
28
+ pairs
29
+ end
30
+
31
+ def get_column(attribute, table = nil)
32
+ attribute = attribute.to_s
33
+ if attribute.include?(".")
34
+ table, column = attribute.split(".", 2)
35
+ else
36
+ column = attribute
37
+ end
38
+ raise(ArgumentError, "Missing table") if table.nil?
39
+
40
+ c = ActiveRecord::Base.connection.columns(table).find { |c| c.name == column }
41
+ raise(StandardError, "Column #{column} does not exist in table #{table}") unless c
42
+
43
+ c
44
+ rescue ActiveRecord::StatementInvalid
45
+ raise(StandardError, "Table #{table} does not exist")
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YiffSpace
4
+ module Search
5
+ VERSION = "0.0.1"
6
+ end
7
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require("yiffspace/core")
4
+ require("yiffspace/ext")
5
+ require("yiffspace/user")
6
+ require("zeitwerk")
7
+
8
+ module YiffSpace
9
+ module Search
10
+ end
11
+ end
12
+
13
+ loader = Zeitwerk::Loader.for_gem_extension(YiffSpace)
14
+ loader.inflector.inflect({ "query_dsl" => "QueryDSL" })
15
+ loader.setup
metadata ADDED
@@ -0,0 +1,126 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: yiffspace-search
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: yiffspace-core
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: 0.2.0
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: 0.2.0
40
+ - !ruby/object:Gem::Dependency
41
+ name: yiffspace-ext
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: 0.0.1
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: 0.0.1
54
+ - !ruby/object:Gem::Dependency
55
+ name: yiffspace-user
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: 0.0.1
61
+ type: :runtime
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: 0.0.1
68
+ - !ruby/object:Gem::Dependency
69
+ name: zeitwerk
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '2.6'
75
+ type: :runtime
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '2.6'
82
+ description: Query-param search DSL (YiffSpace::Search::*), for https://yiff.space
83
+ and related projects
84
+ email:
85
+ - hewwo@yiff.rocks
86
+ executables: []
87
+ extensions: []
88
+ extra_rdoc_files: []
89
+ files:
90
+ - CHANGELOG.md
91
+ - LICENSE
92
+ - README.md
93
+ - Rakefile
94
+ - lib/yiffspace/search.rb
95
+ - lib/yiffspace/search/query_builder.rb
96
+ - lib/yiffspace/search/query_dsl.rb
97
+ - lib/yiffspace/search/query_helper.rb
98
+ - lib/yiffspace/search/version.rb
99
+ homepage: https://yiff.space
100
+ licenses:
101
+ - MIT
102
+ metadata:
103
+ allowed_push_host: https://rubygems.org
104
+ homepage_uri: https://yiff.space
105
+ source_code_uri: https://github.com/YiffSpace/Gem/tree/master/yiffspace-search
106
+ changelog_uri: https://github.com/YiffSpace/Gem/blob/master/yiffspace-search/CHANGELOG.md
107
+ rubygems_mfa_required: 'true'
108
+ rdoc_options: []
109
+ require_paths:
110
+ - lib
111
+ required_ruby_version: !ruby/object:Gem::Requirement
112
+ requirements:
113
+ - - ">="
114
+ - !ruby/object:Gem::Version
115
+ version: 3.4.1
116
+ required_rubygems_version: !ruby/object:Gem::Requirement
117
+ requirements:
118
+ - - ">="
119
+ - !ruby/object:Gem::Version
120
+ version: '0'
121
+ requirements: []
122
+ rubygems_version: 3.6.2
123
+ specification_version: 4
124
+ summary: Query-param search DSL (YiffSpace::Search::*), for https://yiff.space and
125
+ related projects
126
+ test_files: []