yuri-ita 0.2.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 +7 -0
- data/LICENSE +19 -0
- data/lib/yuri-ita.rb +1 -0
- data/lib/yuriita/and_combination.rb +22 -0
- data/lib/yuriita/assembler.rb +15 -0
- data/lib/yuriita/clauses/dynamic.rb +18 -0
- data/lib/yuriita/clauses/filter.rb +19 -0
- data/lib/yuriita/clauses/identity.rb +9 -0
- data/lib/yuriita/clauses/search.rb +20 -0
- data/lib/yuriita/collection.rb +60 -0
- data/lib/yuriita/configuration.rb +31 -0
- data/lib/yuriita/definitions/dynamic.rb +38 -0
- data/lib/yuriita/definitions/exclusive.rb +32 -0
- data/lib/yuriita/definitions/multiple.rb +28 -0
- data/lib/yuriita/definitions/scope.rb +33 -0
- data/lib/yuriita/definitions/single.rb +31 -0
- data/lib/yuriita/dynamic_filter.rb +18 -0
- data/lib/yuriita/errors/collection/action_not_found.rb +14 -0
- data/lib/yuriita/errors/error.rb +4 -0
- data/lib/yuriita/errors/parse_error.rb +4 -0
- data/lib/yuriita/executor.rb +17 -0
- data/lib/yuriita/expression_filter.rb +18 -0
- data/lib/yuriita/inputs/expression.rb +13 -0
- data/lib/yuriita/inputs/keyword.rb +9 -0
- data/lib/yuriita/lexer.rb +21 -0
- data/lib/yuriita/option.rb +19 -0
- data/lib/yuriita/or_combination.rb +23 -0
- data/lib/yuriita/parser.rb +61 -0
- data/lib/yuriita/query.rb +57 -0
- data/lib/yuriita/query_builder.rb +23 -0
- data/lib/yuriita/result.rb +26 -0
- data/lib/yuriita/route.rb +37 -0
- data/lib/yuriita/router.rb +26 -0
- data/lib/yuriita/routing.rb +19 -0
- data/lib/yuriita/runner.rb +19 -0
- data/lib/yuriita/search.rb +13 -0
- data/lib/yuriita/search_filter.rb +26 -0
- data/lib/yuriita/selects/all_or_explicit.rb +28 -0
- data/lib/yuriita/selects/exclusive.rb +48 -0
- data/lib/yuriita/selects/multiple.rb +28 -0
- data/lib/yuriita/selects/single.rb +49 -0
- data/lib/yuriita/table.rb +84 -0
- data/lib/yuriita/version.rb +3 -0
- data/lib/yuriita.rb +19 -0
- metadata +230 -0
@@ -0,0 +1,57 @@
|
|
1
|
+
module Yuriita
|
2
|
+
class Query
|
3
|
+
include Enumerable
|
4
|
+
|
5
|
+
attr_reader :inputs
|
6
|
+
|
7
|
+
def initialize(inputs: [])
|
8
|
+
@inputs = inputs
|
9
|
+
end
|
10
|
+
|
11
|
+
def ==(other)
|
12
|
+
other.is_a?(self.class) && other.inputs == inputs
|
13
|
+
end
|
14
|
+
|
15
|
+
def keywords
|
16
|
+
inputs.select{ |input| input.is_a?(Yuriita::Inputs::Keyword) }
|
17
|
+
end
|
18
|
+
|
19
|
+
def each(&block)
|
20
|
+
block or return enum_for(__method__) { size }
|
21
|
+
inputs.each(&block)
|
22
|
+
self
|
23
|
+
end
|
24
|
+
|
25
|
+
def add_input(input)
|
26
|
+
inputs << input
|
27
|
+
self
|
28
|
+
end
|
29
|
+
alias << add_input
|
30
|
+
|
31
|
+
def delete(input)
|
32
|
+
inputs.delete(input)
|
33
|
+
self
|
34
|
+
end
|
35
|
+
|
36
|
+
def delete_if
|
37
|
+
block_given? or return enum_for(__method__) { size }
|
38
|
+
select { |input| yield input }.each { |input| inputs.delete(input) }
|
39
|
+
self
|
40
|
+
end
|
41
|
+
|
42
|
+
def size
|
43
|
+
inputs.size
|
44
|
+
end
|
45
|
+
alias length size
|
46
|
+
|
47
|
+
def initialize_dup(original)
|
48
|
+
super
|
49
|
+
@inputs = original.instance_variable_get(:@inputs).dup
|
50
|
+
end
|
51
|
+
|
52
|
+
def initialize_clone(original)
|
53
|
+
super
|
54
|
+
@inputs = original.instance_variable_get(:@inputs).clone
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Yuriita
|
2
|
+
class QueryBuilder
|
3
|
+
def self.build(input)
|
4
|
+
new.build(input)
|
5
|
+
end
|
6
|
+
|
7
|
+
def initialize(lexer: Lexer, parser: Parser)
|
8
|
+
@lexer = Lexer
|
9
|
+
@parser = Parser
|
10
|
+
end
|
11
|
+
|
12
|
+
def build(input)
|
13
|
+
tokens = lexer.lex(input)
|
14
|
+
parser.parse(tokens)
|
15
|
+
rescue RLTK::LexingError, RLTK::NotInLanguage, RLTK::BadToken, EOFError
|
16
|
+
raise ParseError
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
attr_reader :lexer, :parser
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Yuriita
|
2
|
+
class Result
|
3
|
+
def self.error(exception)
|
4
|
+
new(exception: exception)
|
5
|
+
end
|
6
|
+
|
7
|
+
def self.success(relation)
|
8
|
+
new(relation: relation)
|
9
|
+
end
|
10
|
+
|
11
|
+
attr_reader :relation, :exception
|
12
|
+
|
13
|
+
def initialize(relation: nil, exception: nil)
|
14
|
+
@relation = relation
|
15
|
+
@exception = exception
|
16
|
+
|
17
|
+
if @relation.nil? && @exception.nil?
|
18
|
+
raise ArgumentError, "Must provide a relation or an exception"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def successful?
|
23
|
+
exception.nil?
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Yuriita
|
2
|
+
class Route
|
3
|
+
attr_reader :collection
|
4
|
+
|
5
|
+
def initialize(collection, matcher, to)
|
6
|
+
@collection = collection
|
7
|
+
@matcher = matcher
|
8
|
+
@to = to
|
9
|
+
end
|
10
|
+
|
11
|
+
def match?(input)
|
12
|
+
case matcher
|
13
|
+
when String
|
14
|
+
# TODO better matching
|
15
|
+
# This won't work for string matching becuase to_s will use quotes
|
16
|
+
# Could we parse it and compare the queries? This would maybe let us
|
17
|
+
# catch parse exceptions
|
18
|
+
#
|
19
|
+
# If we do parse the matcher we could warn if it resulted in more than
|
20
|
+
# two inputs.
|
21
|
+
input.to_s == matcher
|
22
|
+
when Proc
|
23
|
+
matcher.call(input)
|
24
|
+
else
|
25
|
+
matcher.match?(input)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def action
|
30
|
+
to
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
attr_reader :matcher, :to
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Yuriita
|
2
|
+
class Router
|
3
|
+
def initialize
|
4
|
+
@routes = []
|
5
|
+
end
|
6
|
+
|
7
|
+
def append_route(collection, matcher, to:)
|
8
|
+
routes.append Route.new(collection, matcher, to)
|
9
|
+
end
|
10
|
+
|
11
|
+
def route(input, relation)
|
12
|
+
if route = match_for(input)
|
13
|
+
collection = route.collection
|
14
|
+
collection.dispatch(route.action, input, relation)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def match_for(input)
|
19
|
+
routes.detect { |route| route.match?(input) }
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
attr_reader :routes
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Yuriita
|
2
|
+
module Routing
|
3
|
+
extend ::ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
cattr_accessor :router, default: Yuriita::Router.new
|
7
|
+
end
|
8
|
+
|
9
|
+
class_methods do
|
10
|
+
def routing(matcher, to:)
|
11
|
+
router.append_route(self, matcher, to: to)
|
12
|
+
end
|
13
|
+
|
14
|
+
def route(input, relation)
|
15
|
+
router.route(input, relation)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Yuriita
|
2
|
+
class Runner
|
3
|
+
def initialize(relation:, configuration:, **options)
|
4
|
+
@relation = relation
|
5
|
+
@configuration = configuration
|
6
|
+
@executor = options.fetch(:executor, Executor)
|
7
|
+
@assembler = options.fetch(:assembler, Assembler)
|
8
|
+
end
|
9
|
+
|
10
|
+
def run(query)
|
11
|
+
clauses = assembler.new(configuration).build(query)
|
12
|
+
executor.new(clauses).run(relation)
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
attr_reader :relation, :configuration, :executor, :assembler
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Yuriita
|
2
|
+
class SearchFilter
|
3
|
+
attr_reader :input
|
4
|
+
|
5
|
+
def initialize(input:, combination:, &block)
|
6
|
+
@input = input
|
7
|
+
@combination = combination
|
8
|
+
@block = block
|
9
|
+
end
|
10
|
+
|
11
|
+
def apply(relation, keywords)
|
12
|
+
relations = keywords.map do |keyword|
|
13
|
+
block.call(relation, keyword.to_s)
|
14
|
+
end
|
15
|
+
|
16
|
+
combination.new(
|
17
|
+
base_relation: relation,
|
18
|
+
relations: relations,
|
19
|
+
).combine
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
attr_reader :combination, :block
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Yuriita
|
2
|
+
module Selects
|
3
|
+
class AllOrExplicit
|
4
|
+
def initialize(options:, query:)
|
5
|
+
@options = options
|
6
|
+
@query = query
|
7
|
+
end
|
8
|
+
|
9
|
+
def filters
|
10
|
+
active_options.map(&:filter)
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
attr_reader :options, :query
|
16
|
+
|
17
|
+
def active_options
|
18
|
+
explicit_options.presence || options
|
19
|
+
end
|
20
|
+
|
21
|
+
def explicit_options
|
22
|
+
options.select do |option|
|
23
|
+
query.include?(option.input)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module Yuriita
|
2
|
+
module Selects
|
3
|
+
class Exclusive
|
4
|
+
def initialize(options:, default:, query:)
|
5
|
+
@options = options
|
6
|
+
@default = default
|
7
|
+
@query = query
|
8
|
+
end
|
9
|
+
|
10
|
+
def filter
|
11
|
+
active_option.filter
|
12
|
+
end
|
13
|
+
|
14
|
+
def selected?(option)
|
15
|
+
active_option == option
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
attr_reader :options, :default, :query
|
21
|
+
|
22
|
+
def active_option
|
23
|
+
chosen_option || default
|
24
|
+
end
|
25
|
+
|
26
|
+
def chosen_option
|
27
|
+
input = last_matching_input
|
28
|
+
if input.present?
|
29
|
+
option_for(input)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def last_matching_input
|
34
|
+
matching_inputs.last
|
35
|
+
end
|
36
|
+
|
37
|
+
def matching_inputs
|
38
|
+
query.select do |input|
|
39
|
+
options.any? { |option| option.input == input }
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def option_for(input)
|
44
|
+
options.detect { |option| option.input == input }
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Yuriita
|
2
|
+
module Selects
|
3
|
+
class Multiple
|
4
|
+
def initialize(options:, query:)
|
5
|
+
@options = options
|
6
|
+
@query = query
|
7
|
+
end
|
8
|
+
|
9
|
+
def filters
|
10
|
+
active_options.map(&:filter)
|
11
|
+
end
|
12
|
+
|
13
|
+
def selected?(option)
|
14
|
+
active_options.include?(option)
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
attr_reader :options, :query
|
20
|
+
|
21
|
+
def active_options
|
22
|
+
options.select do |option|
|
23
|
+
query.include?(option.input)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module Yuriita
|
2
|
+
module Selects
|
3
|
+
class Single
|
4
|
+
def initialize(options:, query:)
|
5
|
+
@options = options
|
6
|
+
@query = query
|
7
|
+
end
|
8
|
+
|
9
|
+
def filter
|
10
|
+
if active_option.present?
|
11
|
+
active_option.filter
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def selected?(option)
|
16
|
+
if active_option.present?
|
17
|
+
active_option == option
|
18
|
+
else
|
19
|
+
false
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
attr_reader :options, :query
|
26
|
+
|
27
|
+
def active_option
|
28
|
+
input = last_matching_input
|
29
|
+
if input.present?
|
30
|
+
option_for(input)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def last_matching_input
|
35
|
+
matching_inputs.last
|
36
|
+
end
|
37
|
+
|
38
|
+
def matching_inputs
|
39
|
+
query.select do |input|
|
40
|
+
options.any? { |option| option.input == input }
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def option_for(input)
|
45
|
+
options.detect { |option| option.input == input }
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
require "active_support"
|
2
|
+
require "active_model"
|
3
|
+
require "yuriita/runner"
|
4
|
+
|
5
|
+
module Yuriita
|
6
|
+
class Table
|
7
|
+
include ActiveModel::Model
|
8
|
+
|
9
|
+
SPACE = " ".freeze
|
10
|
+
EMPTY_STRING = "".freeze
|
11
|
+
|
12
|
+
def initialize(
|
13
|
+
relation:,
|
14
|
+
params:,
|
15
|
+
configuration:,
|
16
|
+
param_key: :q
|
17
|
+
)
|
18
|
+
@relation = relation
|
19
|
+
@params = params
|
20
|
+
@configuration = configuration
|
21
|
+
@param_key = param_key
|
22
|
+
@search = Yuriita::Search.new(self, @param_key)
|
23
|
+
end
|
24
|
+
|
25
|
+
attr_reader :search
|
26
|
+
|
27
|
+
def q
|
28
|
+
output =
|
29
|
+
if user_input?
|
30
|
+
user_input
|
31
|
+
else
|
32
|
+
default_input
|
33
|
+
end
|
34
|
+
|
35
|
+
if output.blank?
|
36
|
+
EMPTY_STRING
|
37
|
+
else
|
38
|
+
output + SPACE
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def items
|
43
|
+
Runner.new(relation: relation, configuration: configuration).run(query)
|
44
|
+
end
|
45
|
+
|
46
|
+
def filtered?
|
47
|
+
user_input? && user_query != default_query
|
48
|
+
end
|
49
|
+
|
50
|
+
def default_input
|
51
|
+
configuration.default_input
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
attr_reader :relation, :params, :configuration, :param_key
|
57
|
+
|
58
|
+
def query
|
59
|
+
if user_input?
|
60
|
+
user_query
|
61
|
+
else
|
62
|
+
default_query
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def user_input
|
67
|
+
params.fetch(param_key, EMPTY_STRING)
|
68
|
+
end
|
69
|
+
|
70
|
+
def user_input?
|
71
|
+
params.key?(param_key)
|
72
|
+
end
|
73
|
+
|
74
|
+
def default_query
|
75
|
+
QueryBuilder.build(default_input)
|
76
|
+
end
|
77
|
+
|
78
|
+
def user_query
|
79
|
+
QueryBuilder.build(user_input)
|
80
|
+
rescue ParseError
|
81
|
+
Query.new
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
data/lib/yuriita.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
require "zeitwerk"
|
2
|
+
loader = Zeitwerk::Loader.for_gem
|
3
|
+
loader.ignore("#{__dir__}/yuri-ita.rb")
|
4
|
+
loader.collapse("#{__dir__}/yuriita/errors")
|
5
|
+
loader.setup
|
6
|
+
|
7
|
+
module Yuriita
|
8
|
+
def self.filter(relation, input, configuration)
|
9
|
+
query = build_query(input)
|
10
|
+
relation = Runner.new(relation: relation, configuration: configuration).run(query)
|
11
|
+
Result.success(relation)
|
12
|
+
rescue ParseError => exception
|
13
|
+
Result.error(exception)
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.build_query(input)
|
17
|
+
QueryBuilder.build(input)
|
18
|
+
end
|
19
|
+
end
|