revector 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: f00522f0cf5e6aabfadf23fe8a8892497a3e7c0c9abb2ee64b0b4b2f7b3b690f
4
+ data.tar.gz: 7bf974dc51348cacb8ae6bd9d148478185b7f5cb8df306cca645686d4468f49f
5
+ SHA512:
6
+ metadata.gz: eaee10f684d1a0bd9c66dae55c63a62c33489fe64e1a0293f1e95e99822d99821a4c71d0693d6f10eccb0045182b8f0e642e0298a51a1865b8bcf042367d7979
7
+ data.tar.gz: 3f633dc545f1a97ca2db9ded7c93fef8da5e086aebaae7fc2ad5df72043bd1be85a469f631274d34b21a217b86bc57bb08958009b61b6d89b48ea7bb66c57bdd
data/.editorconfig ADDED
@@ -0,0 +1,12 @@
1
+ # EditorConfig is awesome: https://EditorConfig.org
2
+
3
+ # top-most EditorConfig file
4
+ root = true
5
+
6
+ [*]
7
+ indent_style = space
8
+ indent_size = 2
9
+ end_of_line = lf
10
+ charset = utf-8
11
+ trim_trailing_whitespace = false
12
+ insert_final_newline = false
@@ -0,0 +1,25 @@
1
+ name: ci
2
+
3
+ on: [push]
4
+
5
+ permissions:
6
+ contents: read
7
+
8
+ jobs:
9
+ rspec:
10
+ runs-on: ubuntu-latest
11
+ strategy:
12
+ matrix:
13
+ ruby-version: ['2.7', '3.0', '3.1', 'head']
14
+
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+
18
+ - name: Set up Ruby
19
+ uses: ruby/setup-ruby@v1
20
+ with:
21
+ ruby-version: ${{ matrix.ruby-version }}
22
+ bundler-cache: true
23
+
24
+ - name: Run RSpec
25
+ run: bundle exec rspec --color
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+
11
+ # rspec failure tracking
12
+ .rspec_status
13
+
14
+ /.idea/
15
+ .byebug_history
16
+ .DS_store
17
+ *.gem
data/.rubocop.yml ADDED
@@ -0,0 +1,64 @@
1
+ Style/Documentation:
2
+ Enabled: false
3
+
4
+ Style/DocumentationMethod:
5
+ Enabled: false
6
+
7
+ Style/ModuleFunction:
8
+ Enabled: false
9
+
10
+ Style/Lambda:
11
+ Enabled: false
12
+
13
+ Metrics/ClassLength:
14
+ Enabled: false
15
+
16
+ Metrics/MethodLength:
17
+ Max: 15
18
+
19
+ Layout/LineLength:
20
+ Max: 150
21
+
22
+ Layout/FirstArrayElementIndentation:
23
+ Enabled: false
24
+
25
+ Layout/MultilineMethodCallIndentation:
26
+ Enabled: false
27
+
28
+ Style/MutableConstant:
29
+ Enabled: true
30
+
31
+ Layout/FirstHashElementIndentation:
32
+ Enabled: false
33
+
34
+ Style/FrozenStringLiteralComment:
35
+ Enabled: true
36
+ SafeAutoCorrect: true
37
+ Exclude:
38
+ - bin/**/*
39
+ - config.ru
40
+ - Gemfile
41
+ - Rakefile
42
+
43
+ AllCops:
44
+ DisabledByDefault: false
45
+ NewCops: enable
46
+ TargetRubyVersion: 3.1
47
+ Exclude:
48
+ - 'spec/factories/*.rb'
49
+ - 'db/schema.rb'
50
+
51
+ Lint/AmbiguousBlockAssociation:
52
+ Exclude:
53
+ - 'spec/**/*.rb'
54
+
55
+ Metrics/BlockLength:
56
+ Exclude:
57
+ - 'spec/**/*.rb'
58
+
59
+ Style/PercentLiteralDelimiters:
60
+ PreferredDelimiters:
61
+ '%i': '()'
62
+
63
+ Lint/DeprecatedConstants:
64
+ Enabled: true
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 3.3.2
@@ -0,0 +1,3 @@
1
+ {
2
+ "vscode-run-rspec-file.custom-command": "bundle exec rspec --color"
3
+ }
data/CHANGELOG.md ADDED
@@ -0,0 +1,7 @@
1
+ ## Changelog
2
+
3
+ ### v0.1.0
4
+
5
+ #### Chores And Housekeeping
6
+
7
+ - chore: re-name libraty to make sense and avoid conflict with ruby modules @thadeu
@@ -0,0 +1,74 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ In the interest of fostering an open and welcoming environment, we as
6
+ contributors and maintainers pledge to making participation in our project and
7
+ our community a harassment-free experience for everyone, regardless of age, body
8
+ size, disability, ethnicity, gender identity and expression, level of experience,
9
+ nationality, personal appearance, race, religion, or sexual identity and
10
+ orientation.
11
+
12
+ ## Our Standards
13
+
14
+ Examples of behavior that contributes to creating a positive environment
15
+ include:
16
+
17
+ * Using welcoming and inclusive language
18
+ * Being respectful of differing viewpoints and experiences
19
+ * Gracefully accepting constructive criticism
20
+ * Focusing on what is best for the community
21
+ * Showing empathy towards other community members
22
+
23
+ Examples of unacceptable behavior by participants include:
24
+
25
+ * The use of sexualized language or imagery and unwelcome sexual attention or
26
+ advances
27
+ * Trolling, insulting/derogatory comments, and personal or political attacks
28
+ * Public or private harassment
29
+ * Publishing others' private information, such as a physical or electronic
30
+ address, without explicit permission
31
+ * Other conduct which could reasonably be considered inappropriate in a
32
+ professional setting
33
+
34
+ ## Our Responsibilities
35
+
36
+ Project maintainers are responsible for clarifying the standards of acceptable
37
+ behavior and are expected to take appropriate and fair corrective action in
38
+ response to any instances of unacceptable behavior.
39
+
40
+ Project maintainers have the right and responsibility to remove, edit, or
41
+ reject comments, commits, code, wiki edits, issues, and other contributions
42
+ that are not aligned to this Code of Conduct, or to ban temporarily or
43
+ permanently any contributor for other behaviors that they deem inappropriate,
44
+ threatening, offensive, or harmful.
45
+
46
+ ## Scope
47
+
48
+ This Code of Conduct applies both within project spaces and in public spaces
49
+ when an individual is representing the project or its community. Examples of
50
+ representing a project or community include using an official project e-mail
51
+ address, posting via an official social media account, or acting as an appointed
52
+ representative at an online or offline event. Representation of a project may be
53
+ further defined and clarified by project maintainers.
54
+
55
+ ## Enforcement
56
+
57
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
58
+ reported by contacting the project team at tadeuu@gmail.com. All
59
+ complaints will be reviewed and investigated and will result in a response that
60
+ is deemed necessary and appropriate to the circumstances. The project team is
61
+ obligated to maintain confidentiality with regard to the reporter of an incident.
62
+ Further details of specific enforcement policies may be posted separately.
63
+
64
+ Project maintainers who do not follow or enforce the Code of Conduct in good
65
+ faith may face temporary or permanent repercussions as determined by other
66
+ members of the project's leadership.
67
+
68
+ ## Attribution
69
+
70
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71
+ available at [https://contributor-covenant.org/version/1/4][version]
72
+
73
+ [homepage]: https://contributor-covenant.org
74
+ [version]: https://contributor-covenant.org/version/1/4/
data/Gemfile ADDED
@@ -0,0 +1,18 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
4
+
5
+ ruby '>= 2.5.8'
6
+
7
+ gem 'bundler', '>= 1.14'
8
+
9
+ group :test do
10
+ gem 'byebug'
11
+ gem 'rspec'
12
+ gem 'simplecov', require: false
13
+ end
14
+
15
+ group :development do
16
+ gem 'rake', '~> 12.0'
17
+ gem 'rubocop'
18
+ end
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2024 Thadeu Esteves Jr
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,427 @@
1
+ <p align="center">
2
+ <h1 align="center">🔃 revector</h1>
3
+ <p align="center"><i>Simple wrapper to filter array using Ruby and simple predicate conditions</i></p>
4
+ </p>
5
+
6
+ <p align="center">
7
+ <a href="https://rubygems.org/gems/revector">
8
+ <img alt="Gem" src="https://img.shields.io/gem/v/revector.svg">
9
+ </a>
10
+
11
+ <a href="https://github.com/thadeu/revector/actions/workflows/ci.yml">
12
+ <img alt="Build Status" src="https://github.com/thadeu/revector/actions/workflows/ci.yml/badge.svg">
13
+ </a>
14
+ </p>
15
+
16
+
17
+ ## Motivation
18
+
19
+ Because in sometimes, we need filter array passing conditions. This gem simplify this work.
20
+
21
+ ## Documentation <!-- omit in toc -->
22
+
23
+ Version | Documentation
24
+ ---------- | -------------
25
+ unreleased | https://github.com/thadeu/revector/blob/main/README.md
26
+
27
+ ## Table of Contents <!-- omit in toc -->
28
+ - [Installation](#installation)
29
+ - [Configuration](#configuration)
30
+ - [Availables Predicates](#availables-predicates)
31
+ - [Usage](#usage)
32
+ - [Utilities](#utilities)
33
+
34
+ ## Compatibility
35
+
36
+ | kind | branch | ruby |
37
+ | -------------- | ------- | ------------------ |
38
+ | unreleased | main | >= 2.5.8, <= 3.1.x |
39
+
40
+ ## Installation
41
+
42
+ Use bundle
43
+
44
+ ```ruby
45
+ bundle add revector
46
+ ```
47
+
48
+ or add this line to your application's Gemfile.
49
+
50
+ ```ruby
51
+ gem 'revector'
52
+ ```
53
+
54
+ and then, require module
55
+
56
+ ```ruby
57
+ require 'revector'
58
+ ```
59
+
60
+ ## Configuration
61
+
62
+ Without configuration, because we use only Ruby. ❤️
63
+
64
+ ## Availables Predicates for all values
65
+
66
+ | Type | Suffix | Value |
67
+ | ----------- | ----------- | ----------- |
68
+ | Equal | eq | Anywhere |
69
+ | NotEqual | noteq | Anywhere |
70
+ | Contains | cont | Anywhere |
71
+ | NotContains | notcont | Anywhere |
72
+ | Included | in | Anywhere |
73
+ | NotIncluded | notin | Anywhere |
74
+ | Start | start | Anywhere |
75
+ | NotStart | notstart | Anywhere |
76
+ | End | end | Anywhere |
77
+ | NotEnd | notend | Anywhere |
78
+ | LessThan | lt | Anywhere |
79
+ | LessThanEqual | lteq | Anywhere |
80
+ | GreaterThan | gt | Anywhere |
81
+ | GreaterThanEqual | gteq | Anywhere |
82
+
83
+ ## Availables Predicates only when value is Hash
84
+
85
+ > 💡 Below predicates works only when value is Hash
86
+
87
+ | Type | Suffix | Value |
88
+ | ----------- | ----------- | ----------- |
89
+ | NotEqual | not_eq | Hash |
90
+ | NotContains | not_cont | Hash |
91
+ | NotIncluded | not_in | Hash |
92
+ | NotStart | not_start | Hash |
93
+ | NotEnd | not_end | Hash |
94
+
95
+
96
+ ## Usage
97
+
98
+ <details>
99
+ <summary>Think that your data seems like this.</summary>
100
+
101
+ ```ruby
102
+ data = [
103
+ {
104
+ id: 1,
105
+ name: 'Test #1',
106
+ email: 'test1@email1.com',
107
+ schedule: { all_day: true },
108
+ numbers: %w[1 2],
109
+ active: true,
110
+ count: 9
111
+ },
112
+ {
113
+ id: 2,
114
+ name: 'Test #2',
115
+ email: 'test2@email2.com',
116
+ schedule: { all_day: false },
117
+ numbers: %w[3 4],
118
+ active: true,
119
+ count: 10
120
+ },
121
+ {
122
+ id: 3,
123
+ name: 'Test #3',
124
+ email: 'test3@email3.com',
125
+ schedule: { all_day: false },
126
+ numbers: %w[5 6],
127
+ active: false,
128
+ count: 99
129
+ }
130
+ ]
131
+ ```
132
+ </details>
133
+
134
+ You can use one or multiples predicates in your filter. We see some use cases.
135
+
136
+ ### Flexible Use Case (Hash)
137
+
138
+ **Equal**
139
+
140
+ ```ruby
141
+ filters = {
142
+ active: {
143
+ eq: true
144
+ }
145
+ }
146
+
147
+ collection = Revector.swap(data, filters)
148
+ ```
149
+
150
+ **NotEqual**
151
+
152
+ ```ruby
153
+ filters = {
154
+ active: {
155
+ # noteq or not_eq
156
+ not_eq: true
157
+ }
158
+ }
159
+
160
+ collection = Revector.swap(data, filters)
161
+ ```
162
+
163
+ **Nested Hash Paths**
164
+
165
+ ```ruby
166
+ filters = {
167
+ 'schedule.all_day': {
168
+ eq: true
169
+ }
170
+ }
171
+
172
+ collection = Revector.swap(data, filters)
173
+ ```
174
+
175
+ **Nested Array Paths**
176
+
177
+ > Note the `.0` 🎉
178
+
179
+ ```ruby
180
+ filters = {
181
+ 'numbers.0': {
182
+ eq: '3'
183
+ }
184
+ }
185
+
186
+ collection = Revector.swap(data, filters)
187
+ ```
188
+
189
+ ```ruby
190
+ filters = {
191
+ numbers: {
192
+ in: '3' # or in: ['3']
193
+ }
194
+ }
195
+
196
+ collection = Revector.swap(data, filters)
197
+ ```
198
+
199
+ Using default Equal predicate.
200
+
201
+ ```ruby
202
+ Revector.swap(data, numbers: 3)
203
+
204
+ Revector.swap(data, active: true)
205
+
206
+ Revector.swap(data, id: 3)
207
+ ```
208
+
209
+ If array, you can navigate into self, using `property.NUMBER.property`
210
+
211
+ ```ruby
212
+ data = [
213
+ {
214
+ schedules: [
215
+ {
216
+ opened: true,
217
+ all_day: true
218
+ },
219
+ {
220
+ opened: false,
221
+ all_day: true
222
+ }
223
+ ]
224
+ },
225
+ {
226
+ schedules: [
227
+ {
228
+ opened: false,
229
+ all_day: true
230
+ },
231
+ {
232
+ opened: false,
233
+ all_day: true
234
+ }
235
+ ]
236
+ }
237
+ ]
238
+
239
+ filters = {
240
+ 'schedules.0.opened': {
241
+ eq: true
242
+ }
243
+ }
244
+
245
+ # OR
246
+
247
+ filters = {
248
+ 'schedules[0]opened': {
249
+ eq: true
250
+ }
251
+ }
252
+
253
+ # OR
254
+
255
+ filters = {
256
+ 'schedules.[0].opened': {
257
+ eq: true
258
+ }
259
+ }
260
+
261
+ collection = Revector.swap(data, filters)
262
+
263
+ # [{ schedules: [{ opened: true, all_day: true }, { opened: false, all_day: true }] }]
264
+ ```
265
+
266
+ Amazing, you can pass a Callable value as value, like this.
267
+
268
+ ```ruby
269
+ filters = {
270
+ 'schedules.[0].opened': {
271
+ eq: -> { true }
272
+ }
273
+ }
274
+
275
+ # OR a Module
276
+
277
+ module ActiveTruthy
278
+ def self.call = true
279
+ end
280
+
281
+ module NumbersAvailable
282
+ def self.call = %w[1 2]
283
+ end
284
+
285
+ filters = {
286
+ 'schedules.[0].opened': {
287
+ eq: ActiveTruthy
288
+ },
289
+ numbers: {
290
+ in: NumbersAvailable
291
+ }
292
+ }
293
+
294
+ # OR a Class
295
+
296
+ class ActiveFalsey
297
+ def self.call = false
298
+ end
299
+
300
+ filters = {
301
+ 'schedules.[0].opened': {
302
+ eq: ActiveFalsey
303
+ }
304
+ }
305
+
306
+ collection = Revector.swap(data, filters)
307
+ ```
308
+
309
+ **Combine conditions**
310
+
311
+ Yes, you can combine one or multiple predicates to filter you array.
312
+
313
+
314
+ ```ruby
315
+ filters = {
316
+ active: { eq: true },
317
+ numbers: {
318
+ in: %w[5],
319
+ not_in: '10'
320
+ },
321
+ email: {
322
+ cont: 'email1',
323
+ not_cont: '@gmail'
324
+ },
325
+ 'schedule.all_day': {
326
+ in: [true, false]
327
+ }
328
+ }
329
+
330
+ collection = Revector.swap(data, filters)
331
+ ```
332
+
333
+ ### Querystring Use Case
334
+
335
+ **Equal**
336
+
337
+ ```ruby
338
+ filters = { active_eq: true }
339
+
340
+ collection = Revector.swap(data, filters)
341
+ ```
342
+
343
+ **NotEqual**
344
+
345
+ ```ruby
346
+ filters = { active_noteq: true }
347
+
348
+ collection = Revector.swap(data, filters)
349
+ ```
350
+
351
+ **Nested Hash Paths**
352
+
353
+ ```ruby
354
+ filters = { 'schedule.all_day_eq': false }
355
+
356
+ collection = Revector.swap(data, filters)
357
+ ```
358
+
359
+ **Nested Array Paths**
360
+
361
+ > Note the `.0` 🎉
362
+
363
+ ```ruby
364
+ filters = { 'numbers.0_eq': '3' }
365
+
366
+ collection = Revector.swap(data, filters)
367
+ ```
368
+
369
+ ```ruby
370
+ filters = { numbers_in: ['1'] }
371
+
372
+ collection = Revector.swap(data, filters)
373
+
374
+ expect(collection.size).to eq(1)
375
+ ```
376
+
377
+ **Combine conditions**
378
+
379
+ Yes, you can combine one or multiple predicates to filter you array.
380
+
381
+
382
+ ```ruby
383
+ filters = {
384
+ active_noteq: true,
385
+ numbers_in: %w[5],
386
+ email_cont: 'test3',
387
+ 'schedule.all_day_eq': false
388
+ }
389
+
390
+ collection = Revector.swap(data, filters)
391
+ ```
392
+
393
+ **Receive querystring in your route**
394
+
395
+ Like Ransack, imagine that you receive an querystring and you want to filter your Array. So, you can to do something like this.
396
+
397
+ > ⚠️ But security is your responsability, ok? Let's go!
398
+
399
+ ```ruby
400
+ # receive querystring in your route
401
+ querystring = "active_noteq=true&numbers_in=5&email_cont=test3&schedule.all_day_eq=false"
402
+
403
+ # parse querystring and transform to Hash
404
+ params = URI.decode_www_form(querystring).to_h
405
+
406
+ # filter your collection using params directly.
407
+ collection = Revector.swap(data, filters)
408
+
409
+ # Beautiful, right? 🎉
410
+ ```
411
+
412
+ [⬆️ &nbsp;Back to Top](#table-of-contents-)
413
+
414
+ ## Development
415
+
416
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `bundle exec rspec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
417
+
418
+ To install this gem onto your local machine, run `bundle install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
419
+
420
+ ## Contributing
421
+
422
+ Bug reports and pull requests are welcome on GitHub at https://github.com/thadeu/revector. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/thadeu/revector/blob/master/CODE_OF_CONDUCT.md).
423
+
424
+
425
+ ## License
426
+
427
+ 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,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
+ require 'rubocop/rake_task'
6
+
7
+ RSpec::Core::RakeTask.new(:spec)
8
+
9
+ RuboCop::RakeTask.new(:rubocop) do |t|
10
+ t.options = ['--display-cop-names']
11
+ end
12
+
13
+ task default: %i(spec rubocop)
data/bin/console ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'bundler/setup'
5
+ require 'revector'
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ # (If you use this, don't forget to add pry to your Gemfile!)
11
+ # require "pry"
12
+ # Pry.start
13
+
14
+ require 'irb'
15
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env bash
2
+
3
+ set -euo pipefail
4
+ IFS=$'\n\t'
5
+ set -vx
6
+
7
+ bundle install
8
+
9
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Revector
4
+ module Contains
5
+ def self.check!(item, iteratee, value)
6
+ fetched_value = Utility::TryFetchOrBlank[item, iteratee]
7
+ return false unless fetched_value
8
+
9
+ compare(fetched_value, value)
10
+ end
11
+
12
+ def self.compare(fetched_value, value)
13
+ Array(value).any? { |v| fetched_value.to_s.match(/#{v}/) }
14
+ end
15
+ end
16
+
17
+ module NotContains
18
+ def self.check!(item, iteratee, value)
19
+ !Contains.check!(item, iteratee, value)
20
+ end
21
+
22
+ def self.compare(fetched_value, value)
23
+ !Contains.compare(fetched_value, value)
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Revector
4
+ module Endify
5
+ def self.check!(item, iteratee, value)
6
+ fetched_value = Utility::TryFetchOrBlank[item, iteratee]
7
+ return false unless fetched_value
8
+
9
+ compare(fetched_value, value)
10
+ end
11
+
12
+ def self.compare(fetched_value, value)
13
+ regex = /#{value}$/
14
+ fetched_value.to_s.match?(regex)
15
+ end
16
+ end
17
+
18
+ module NotEndify
19
+ def self.check!(item, iteratee, value)
20
+ !Endify.check!(item, iteratee, value)
21
+ end
22
+
23
+ def self.compare(fetched_value, value)
24
+ !Endify.compare(fetched_value, value)
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Revector
4
+ module Equal
5
+ def self.check!(item, iteratee, expected_value)
6
+ compare(expected_value, Utility::TryFetchOrBlank[item, iteratee])
7
+ end
8
+
9
+ def self.compare(value, expected_value)
10
+ value == expected_value
11
+ end
12
+ end
13
+
14
+ module NotEqual
15
+ def self.check!(item, iteratee, value)
16
+ !Equal.check!(item, iteratee, value)
17
+ end
18
+
19
+ def self.compare(value, expected_value)
20
+ !Equal.compare(value, expected_value)
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Revector
4
+ module GreaterThan
5
+ def self.check!(item, iteratee, right)
6
+ left = Utility::TryFetchOrBlank[item, iteratee]
7
+ return false unless left
8
+
9
+ compare(left, right)
10
+ end
11
+
12
+ def self.compare(left, right)
13
+ left > right
14
+ end
15
+ end
16
+
17
+ module GreaterThanEqual
18
+ def self.check!(item, iteratee, right)
19
+ left = Utility::TryFetchOrBlank[item, iteratee]
20
+ return false unless left
21
+
22
+ compare(left, right)
23
+ end
24
+
25
+ def self.compare(left, right)
26
+ left >= right
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Revector
4
+ module Included
5
+ def self.check!(item, iteratee, right)
6
+ left = Array(Utility::TryFetchOrBlank[item, iteratee]).compact
7
+ return false unless Array(left).count > 0
8
+
9
+ compare(left, right)
10
+ end
11
+
12
+ def self.compare(arr, expected_array)
13
+ Array(arr).any? do |item|
14
+ Array(expected_array).include?(item)
15
+ end
16
+ end
17
+ end
18
+
19
+ module NotIncluded
20
+ def self.check!(item, iteratee, value)
21
+ !Included.check!(item, iteratee, value)
22
+ end
23
+
24
+ def self.compare(arr, expected_array)
25
+ !Included.compare(arr, expected_array)
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Revector
4
+ module LessThan
5
+ def self.check!(item, iteratee, right)
6
+ left = Utility::TryFetchOrBlank[item, iteratee]
7
+ return false unless left
8
+
9
+ compare(left, right)
10
+ end
11
+
12
+ def self.compare(left, right)
13
+ left < right
14
+ end
15
+ end
16
+
17
+ module LessThanEqual
18
+ def self.check!(item, iteratee, right)
19
+ left = Utility::TryFetchOrBlank[item, iteratee]
20
+ return false unless left
21
+
22
+ compare(left, right)
23
+ end
24
+
25
+ def self.compare(left, right)
26
+ left <= right
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Revector
4
+ module Startify
5
+ def self.check!(item, iteratee, right)
6
+ left = Utility::TryFetchOrBlank[item, iteratee]
7
+ return false unless left
8
+
9
+ regex = /^#{right}/
10
+ left.to_s.match?(regex)
11
+ end
12
+
13
+ def self.compare(left, right)
14
+ regex = /^#{right}/
15
+ left.to_s.match?(regex)
16
+ end
17
+ end
18
+
19
+ module NotStartify
20
+ def self.check!(item, iteratee, right)
21
+ !Startify.check!(item, iteratee, right)
22
+ end
23
+
24
+ def self.compare(left, right)
25
+ !Startify.compare(left, right)
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Revector
4
+ module Utility
5
+ module Keys
6
+ extend self
7
+
8
+ DELIMITER_REGEX = /\[|\]|\./
9
+
10
+ def to_ary(key)
11
+ return [] if key.empty?
12
+
13
+ return key.flat_map { |k| to_ary(k) } if key.is_a?(::Array)
14
+
15
+ key.to_s.split(DELIMITER_REGEX).reject(&:empty?)
16
+ end
17
+ alias [] to_ary
18
+
19
+ def to_dig(key)
20
+ enforce_level = ->(level) do
21
+ if level.is_a?(Integer) || level.match?(/^\d$/)
22
+ level.to_i
23
+ else
24
+ level.to_sym
25
+ end
26
+ end
27
+
28
+ Keys[key].map(&enforce_level)
29
+ end
30
+ end
31
+
32
+ # ### Utility::TryFetchOrBlank
33
+ # `fetch value into hash, like Lodash.get`
34
+ #
35
+ # ````
36
+ # hash = { a: 1, b: { c: 2 }, d: ['1'] }
37
+ # Utility::TryFetchOrBlank.call(hash, :b, :c)
38
+ # ````
39
+ #
40
+ # ````
41
+ # hash = { a: 1, b: { c: 2 }, d: ['1'] }
42
+ # Utility::TryFetchOrBlank[hash, 'd.0']
43
+ # ````
44
+ #
45
+ # ````
46
+ # hash = { a: 1, b: { c: 2 }, d: [{ e: 3 }] }
47
+ # Utility::TryFetchOrBlank.(hash, 'd.0.e')
48
+
49
+ # hash = { a: 1, b: { c: 2 }, d: [{ e: 3 }] }
50
+ # Utility::TryFetchOrBlank.(hash, 'd[0]e')
51
+
52
+ # hash = { a: 1, b: { c: 2 }, d: [{ e: 3 }] }
53
+ # Utility::TryFetchOrBlank.(hash, 'd.[0].e')
54
+ # ````
55
+ TryFetchOrBlank = ->(data, *arr_keys) do
56
+ reducer = ->(memo, key) do
57
+ symbol_keys = *Keys.to_dig(key)
58
+
59
+ deep_value = symbol_keys.reduce(memo) do |acc, attr|
60
+ acc = acc.compact
61
+
62
+ case acc
63
+ in [::Integer, _] | [::String, _] | Integer
64
+ acc[attr]
65
+ in ::Array
66
+ case acc
67
+ in [::Hash, _]
68
+ case attr
69
+ in ::Integer
70
+ acc&.dig(attr)
71
+ else
72
+ acc&.flat_map { |x| x&.dig(attr) }
73
+ end
74
+ else
75
+ case attr
76
+ in ::Integer | ::String
77
+ acc&.dig(attr)
78
+ else
79
+ acc&.flat_map { |x| x.respond_to?(:dig) ? x&.dig(attr) : x }
80
+ end
81
+ end
82
+ in ::Hash
83
+ acc&.dig(attr)
84
+ else
85
+ acc
86
+ end
87
+ end
88
+
89
+ deep_value
90
+ rescue StandardError => e
91
+ nil
92
+ end
93
+
94
+ arr_keys.reduce(data, &reducer)
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Revector
4
+ VERSION = '0.1.0'
5
+ end
data/lib/revector.rb ADDED
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'set'
4
+
5
+ require_relative 'revector/utility'
6
+ require_relative 'revector/predicate/startify'
7
+ require_relative 'revector/predicate/endify'
8
+ require_relative 'revector/predicate/equal'
9
+ require_relative 'revector/predicate/contains'
10
+ require_relative 'revector/predicate/in'
11
+ require_relative 'revector/predicate/less_than'
12
+ require_relative 'revector/predicate/greater_than'
13
+
14
+ class Revector
15
+ def self.swap(bigdata, filters)
16
+ new(bigdata, filters).swap
17
+ end
18
+
19
+ def initialize(bigdata, filters)
20
+ @result = Array(bigdata)
21
+ @filters = filters
22
+ @idx = ::Set.new
23
+ end
24
+
25
+ def swap
26
+ @result.each_with_index do |row, index|
27
+ collected = @filters.collect do |key, condition|
28
+ filter_by(row, key, condition)
29
+ end.flatten.uniq
30
+
31
+ case collected.all?
32
+ in TrueClass then @idx.add(index)
33
+ in FalseClass then @idx.delete(index)
34
+ end
35
+ end
36
+
37
+ @result = @result.values_at(*@idx.to_a)
38
+ end
39
+
40
+ def filter_by(row, key, hash_or_value)
41
+ case hash_or_value
42
+ in ::Hash then find_by_hash(key, hash_or_value, row)
43
+ else find_by_other(key, hash_or_value, row)
44
+ end
45
+ end
46
+
47
+ def find_by_hash(key, pair_condition, row)
48
+ pair_condition.collect do |predicate, value|
49
+ keys = Utility::Keys.to_ary(key)
50
+
51
+ right = Utility::TryFetchOrBlank[row, keys[0..-1]]
52
+ predicatable = Predicate[predicate]
53
+ valueable = value.respond_to?(:call) ? value.call : value
54
+
55
+ case right
56
+ in [*]
57
+ Array(right).any? { |o| predicatable.compare(o, valueable) }
58
+ else
59
+ predicatable.compare(right, valueable)
60
+ end
61
+ end
62
+ end
63
+
64
+ def find_by_other(key, value, row)
65
+ parts = key.to_s.split('_')
66
+
67
+ predicate = Array(parts[-2..]).filter do |pkey|
68
+ next unless PREDICATES.include? pkey
69
+
70
+ parts = parts - [pkey]
71
+
72
+ pkey
73
+ end&.last || :eq
74
+
75
+ iteratee = parts.join('_')
76
+
77
+ predicatable = Predicate[predicate]
78
+ valueable = value.respond_to?(:call) ? value.call : value
79
+
80
+ predicatable.check!(row, iteratee, valueable)
81
+ end
82
+
83
+ Predicate = lambda do |named|
84
+ {
85
+ eq: Equal,
86
+ noteq: NotEqual,
87
+ not_eq: NotEqual,
88
+ cont: Contains,
89
+ notcont: NotContains,
90
+ not_cont: NotContains,
91
+ lt: LessThan,
92
+ lteq: LessThanEqual,
93
+ gt: GreaterThan,
94
+ gteq: GreaterThanEqual,
95
+ start: Startify,
96
+ st: Startify,
97
+ notstart: NotStartify,
98
+ notst: NotStartify,
99
+ not_start: NotStartify,
100
+ not_st: NotStartify,
101
+ end: Endify,
102
+ notend: NotEndify,
103
+ not_end: NotEndify,
104
+ in: Included,
105
+ notin: NotIncluded,
106
+ not_in: NotIncluded
107
+ }[named.to_sym || :eq]
108
+ end
109
+ private_constant :Predicate
110
+
111
+ AFFIRMATIVES = %w[
112
+ eq
113
+ in cont
114
+ lt lteq
115
+ gt gteq
116
+ st start end
117
+ ].freeze
118
+
119
+ NEGATIVES = %w[
120
+ noteq not_eq
121
+ notcont not_cont
122
+ notstart notst not_start not_st
123
+ notend not_end
124
+ notin not_in
125
+ ].freeze
126
+
127
+ PREDICATES = (AFFIRMATIVES + NEGATIVES).freeze
128
+ private_constant :PREDICATES
129
+ end
data/revector.gemspec ADDED
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path('lib', __dir__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+
6
+ require 'revector/version'
7
+
8
+ Gem::Specification.new do |spec|
9
+ spec.name = 'revector'
10
+ spec.version = Revector::VERSION
11
+ spec.authors = ['Thadeu Esteves']
12
+ spec.email = ['tadeuu@gmail.com']
13
+ spec.summary = 'Simple wrapper to filter array using Ruby and simple predicate conditions'
14
+ spec.description = 'Filter collections using predicates like Ransack gem.'
15
+ spec.homepage = 'https://github.com/thadeu/revector'
16
+ spec.license = 'MIT'
17
+
18
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
19
+ f.match(%r{^(test|spec|features)/})
20
+ end
21
+
22
+ spec.required_ruby_version = '>= 2.7.0' # rubocop:disable Gemspec/RequiredRubyVersion
23
+ spec.require_paths = ['lib']
24
+
25
+ spec.metadata['rubygems_mfa_required'] = 'true'
26
+ end
metadata ADDED
@@ -0,0 +1,69 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: revector
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Thadeu Esteves
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2024-12-11 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Filter collections using predicates like Ransack gem.
14
+ email:
15
+ - tadeuu@gmail.com
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - ".editorconfig"
21
+ - ".github/workflows/ci.yml"
22
+ - ".gitignore"
23
+ - ".rubocop.yml"
24
+ - ".ruby-version"
25
+ - ".vscode/settings.json"
26
+ - CHANGELOG.md
27
+ - CODE_OF_CONDUCT.md
28
+ - Gemfile
29
+ - LICENSE
30
+ - README.md
31
+ - Rakefile
32
+ - bin/console
33
+ - bin/setup
34
+ - lib/revector.rb
35
+ - lib/revector/predicate/contains.rb
36
+ - lib/revector/predicate/endify.rb
37
+ - lib/revector/predicate/equal.rb
38
+ - lib/revector/predicate/greater_than.rb
39
+ - lib/revector/predicate/in.rb
40
+ - lib/revector/predicate/less_than.rb
41
+ - lib/revector/predicate/startify.rb
42
+ - lib/revector/utility.rb
43
+ - lib/revector/version.rb
44
+ - revector.gemspec
45
+ homepage: https://github.com/thadeu/revector
46
+ licenses:
47
+ - MIT
48
+ metadata:
49
+ rubygems_mfa_required: 'true'
50
+ post_install_message:
51
+ rdoc_options: []
52
+ require_paths:
53
+ - lib
54
+ required_ruby_version: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ version: 2.7.0
59
+ required_rubygems_version: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ version: '0'
64
+ requirements: []
65
+ rubygems_version: 3.5.9
66
+ signing_key:
67
+ specification_version: 4
68
+ summary: Simple wrapper to filter array using Ruby and simple predicate conditions
69
+ test_files: []