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.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +19 -0
  3. data/lib/yuri-ita.rb +1 -0
  4. data/lib/yuriita/and_combination.rb +22 -0
  5. data/lib/yuriita/assembler.rb +15 -0
  6. data/lib/yuriita/clauses/dynamic.rb +18 -0
  7. data/lib/yuriita/clauses/filter.rb +19 -0
  8. data/lib/yuriita/clauses/identity.rb +9 -0
  9. data/lib/yuriita/clauses/search.rb +20 -0
  10. data/lib/yuriita/collection.rb +60 -0
  11. data/lib/yuriita/configuration.rb +31 -0
  12. data/lib/yuriita/definitions/dynamic.rb +38 -0
  13. data/lib/yuriita/definitions/exclusive.rb +32 -0
  14. data/lib/yuriita/definitions/multiple.rb +28 -0
  15. data/lib/yuriita/definitions/scope.rb +33 -0
  16. data/lib/yuriita/definitions/single.rb +31 -0
  17. data/lib/yuriita/dynamic_filter.rb +18 -0
  18. data/lib/yuriita/errors/collection/action_not_found.rb +14 -0
  19. data/lib/yuriita/errors/error.rb +4 -0
  20. data/lib/yuriita/errors/parse_error.rb +4 -0
  21. data/lib/yuriita/executor.rb +17 -0
  22. data/lib/yuriita/expression_filter.rb +18 -0
  23. data/lib/yuriita/inputs/expression.rb +13 -0
  24. data/lib/yuriita/inputs/keyword.rb +9 -0
  25. data/lib/yuriita/lexer.rb +21 -0
  26. data/lib/yuriita/option.rb +19 -0
  27. data/lib/yuriita/or_combination.rb +23 -0
  28. data/lib/yuriita/parser.rb +61 -0
  29. data/lib/yuriita/query.rb +57 -0
  30. data/lib/yuriita/query_builder.rb +23 -0
  31. data/lib/yuriita/result.rb +26 -0
  32. data/lib/yuriita/route.rb +37 -0
  33. data/lib/yuriita/router.rb +26 -0
  34. data/lib/yuriita/routing.rb +19 -0
  35. data/lib/yuriita/runner.rb +19 -0
  36. data/lib/yuriita/search.rb +13 -0
  37. data/lib/yuriita/search_filter.rb +26 -0
  38. data/lib/yuriita/selects/all_or_explicit.rb +28 -0
  39. data/lib/yuriita/selects/exclusive.rb +48 -0
  40. data/lib/yuriita/selects/multiple.rb +28 -0
  41. data/lib/yuriita/selects/single.rb +49 -0
  42. data/lib/yuriita/table.rb +84 -0
  43. data/lib/yuriita/version.rb +3 -0
  44. data/lib/yuriita.rb +19 -0
  45. 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,13 @@
1
+ require "active_model"
2
+
3
+ module Yuriita
4
+ class Search
5
+ include ActiveModel::Model
6
+
7
+ def initialize(table, param_key)
8
+ define_singleton_method(param_key) do
9
+ table.q
10
+ end
11
+ end
12
+ end
13
+ 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
@@ -0,0 +1,3 @@
1
+ module Yuriita
2
+ VERSION = "0.2.1"
3
+ 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