advanced_search 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ce98b039931b0ce1e6e6bc83e97f40acb7380346f46d74b1e099ed3e703b0e5e
4
- data.tar.gz: af1ac1ee927e44a960d654f4df0760f2ae84e3d049f0e0e0d94748a82d36525c
3
+ metadata.gz: 1a8f4bd11b385b3ca93fe87eec974628aa130f52303de760c46b371ff4c1bf27
4
+ data.tar.gz: 5ff6debc2d699f2f1b0b5fdab32ce3c3e91967abcad46a221b412d9eb9a0e135
5
5
  SHA512:
6
- metadata.gz: db39c98ee186875baae9eb6f65c6211e00fb3a297638799d0a0d1de990700b59ef36cd2a817e563e968f687ad2b7019fd21e63911a1399e6a7264d9cac2a050c
7
- data.tar.gz: 33a2b2497ac03c37227b6ad6ff21389736d4aa649492fd076cadfe79e6cd7c46786f7ae93fad66069ffc0599f6e49cdece540cdef6922dff05a7f21be676ef1b
6
+ metadata.gz: d8cfe5debf530fd434ad5a062e9559479789a847775decb4a2ac80eb4ef06a0bd1517d38e72b11ea6142b2538115063ed0dca415c1c8ccd00f59b901fbd1aa41
7
+ data.tar.gz: 9c6d1331ef90268f8fbe319488283c9ab5cc3de1a7306583230eb46dd11065065b9826aaf383d7ba42071e725cec69792662c9d2214d683db5bc0b26afe19418
data/.gitignore CHANGED
@@ -1,6 +1,7 @@
1
1
  /.bundle/
2
2
  /.yardoc
3
3
  /_yardoc/
4
+ /Gemfile.lock
4
5
  /coverage/
5
6
  /doc/
6
7
  /pkg/
@@ -5,17 +5,22 @@ recommendations of [keepachangelog.com](http://keepachangelog.com/).
5
5
 
6
6
  ## Unreleased
7
7
 
8
- ### Breaking Changes
9
-
10
- - None
11
-
12
- ### Added
13
-
14
- - None
15
-
16
- ### Fixed
17
-
18
- - None
8
+ - Breaking Changes
9
+ - None
10
+ - Added
11
+ - None
12
+ - Fixed
13
+ - None
14
+
15
+ ## 0.0.2 (2018-10-30)
16
+
17
+ - Breaking Changes
18
+ - Instead of an ActiveRecord adapter, just provide (and test) some examples
19
+ of using `find_by_sql`
20
+ - Added
21
+ - None
22
+ - Fixed
23
+ - None
19
24
 
20
25
  ## 0.0.1 (2018-10-03)
21
26
 
data/README.md CHANGED
@@ -1,94 +1,98 @@
1
1
  # AdvancedSearch
2
2
 
3
- Ruby database **query builder** for advanced search. For example, library
4
- catalog, employee phonebook, non-aggregate reports.
3
+ Builds database search queries with complex, nested logical expressions.
4
+
5
+ You provide an [abstract syntax tree][1] representing the search, and
6
+ `AdvancedSearch` builds the search clause of the query (eg. the `where`
7
+ clause, in SQL).
8
+
9
+ | *Good for* | *Not Good For* |
10
+ | ---------------- | -------------- |
11
+ | searches | reports |
12
+ | complex logic expressions | simple "conjunction of all parameters" forms |
13
+ | natural language | 99% of business search forms |
14
+
15
+ Input can be natural language,
16
+
17
+ ```
18
+ 'age less than 40 and ssn equal to 123-45-6789'
19
+ ```
20
+
21
+ or a typical HTML form,
22
+
23
+ ```
24
+ {
25
+ age_lt: 40,
26
+ ssn_eq: '123-45-6789'
27
+ }
28
+ ```
29
+
30
+ You organize and name your parameters however you want. You convert those
31
+ parameters into an `AdvancedSearch::AST`:
32
+
33
+ ```
34
+ and
35
+ / \
36
+ lt eq
37
+ / \ / \
38
+ age 40 ssn '123-45-6789'
39
+ ```
40
+
41
+ A convenient S-expression syntax is provided to help you build your tree:
42
+
43
+ ```
44
+ s(:and,
45
+ s(:lt, s(:id, :age), s(:value, 40),
46
+ s(:eq, s(:id, :ssn), s(:value, '123-45-6789')
47
+ )
48
+ ```
49
+
50
+ `AdvancedSearch` converts your tree into a parameterized search clause (eg. the
51
+ `where` clause, in SQL).
52
+
53
+ ```
54
+ 'where age < $1 and ssn = $2'
55
+ [40, '123-45-6789']
56
+ ```
57
+
58
+ Finally, you do whatever you want with that search clause. Prepare a complete
59
+ query and execute it using `ActiveRecord`'s [find_by_sql][2] method, perhaps?
60
+ Maybe execute it directly using a gem like `pg` or `mysql2`? What you do with
61
+ your query, in the privacy of your own home, is your own business.
62
+
63
+ ## Complete Example
64
+
65
+ There is a complete example using `ActiveRecord`'s [find_by_sql][2] method in
66
+ the test suite. See:
67
+
68
+ ```
69
+ spec/support/activerecord/models
70
+ spec/support/activerecord/searches/library_catalog.rb
71
+ spec/activerecord/searches/library_catalog_spec.rb
72
+ ```
73
+
74
+ ## Adapters
75
+
76
+ | *adapter* | *gem* | *status* |
77
+ | -------------- | -------------- | ------------------- |
78
+ | `PG` | `pg` | Proof of concept |
79
+ | `Mysql2` | `mysql2` | Not yet implemented |
80
+ | `ActiveRecord` | `activerecord` | If only ARel had docs |
81
+
82
+ ## Design Goals
5
83
 
6
84
  - **Simple, not easy.** You'll have to write substantial code to get started,
7
85
  but future changes will be simple.
8
86
  - Requires basic understanding of graph theory, must know what a tree is.
9
- - **Agnostic**: Support for specific databases and ORMs is provided via plugins.
87
+ - **Agnostic**: Support for specific databases and ORMs is provided via *adapters*.
10
88
  - **No dependencies**
11
89
  - Unit tests of your search objects do not need to touch database, so they
12
90
  are very fast.
13
91
 
14
- ## Example 1: Employee Phonebook
15
-
16
- Given a `Hash` of parameters from an HTTP form, build a search query.
17
-
18
- ```ruby
19
- # You write one of these classes for each search form in your application.
20
- # You write this class however you want to. This is one pattern I like.
21
- class PhonebookSearch
22
- def initialize
23
- # We will be building a tree, and the head node is an N-ary
24
- # conjunction. Conjunction is the most common type of search. Disjunction
25
- # is also common.
26
- @head = ::AdvancedSearch::Nodes::And.new
27
-
28
- # Later, we'll traverse that tree using a visitor that knows how to build
29
- # a query for use with the mysql2 gem. Many other visitors are available.
30
- # If you switch databases in the future, you only change the visitor, you
31
- # don't change anything else in this class.
32
- @visitor = ::AdvancedSearch::Visitors::Mysql2.new
33
- end
34
-
35
- # Example params:
36
- #
37
- # {
38
- # age_lt: 40,
39
- # ssn_eq: '123-45-6789'
40
- # }
41
- #
42
- # You can organize your parameters, and name them, however you want. A Hash
43
- # is common.
44
- def search(params)
45
- build_tree(params)
46
- build_query
47
- end
48
-
49
- private
50
-
51
- # Build a tree by dynamically `send`ing the parameter name. Given the params
52
- # above, our tree will look like this:
53
- #
54
- # and
55
- # / \
56
- # lt eq
57
- # / \ / \
58
- # age 40 ssn '123-45-6789'
59
- #
60
- def build_tree(params)
61
- params.each { |k, v| send(k, v) }
62
- end
63
-
64
- # You write one method like this for each filter on your search form. After
65
- # this, our tree looks like:
66
- #
67
- # and
68
- # |
69
- # lt
70
- # / \
71
- # age 40
72
- #
73
- def age_lt(v)
74
- eq = ::AdvancedSearch::Nodes::Lt.new
75
- eq.add_edge(::AdvancedSearch::Nodes::Id.new(:age))
76
- eq.add_edge(::AdvancedSearch::Nodes::Value.new(v))
77
- @head.add_edge(eq)
78
- end
79
-
80
- def ssn_eq(v)
81
- eq = ::AdvancedSearch::Nodes::Eq.new
82
- eq.add_edge(::AdvancedSearch::Nodes::Id.new(:ssn))
83
- eq.add_edge(::AdvancedSearch::Nodes::Value.new(v))
84
- @head.add_edge(eq)
85
- end
86
-
87
- # Returns a query suitable for use with the mysql2 gem, because that's the
88
- # visitor we selected above.
89
- def build_query
90
- @head.accept(@visitor)
91
- @visitor.result
92
- end
93
- end
94
- ```
92
+ ## Roadmap
93
+
94
+ - full-text search
95
+ - "raw" node?
96
+
97
+ [1]: https://en.wikipedia.org/wiki/Abstract_syntax_tree
98
+ [2]: https://api.rubyonrails.org/classes/ActiveRecord/Querying.html#method-i-find_by_sql
@@ -2,9 +2,9 @@ lib = File.expand_path("../lib", __FILE__)
2
2
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
3
  require "advanced_search/version"
4
4
 
5
- Gem::Specification.new do |spec|
5
+ ::Gem::Specification.new do |spec|
6
6
  spec.name = "advanced_search"
7
- spec.version = AdvancedSearch::VERSION
7
+ spec.version = ::AdvancedSearch.gem_version
8
8
  spec.licenses = ['AGPL-3.0']
9
9
  spec.authors = ["Jared Beck"]
10
10
  spec.email = ["jared@jaredbeck.com"]
@@ -17,7 +17,9 @@ Gem::Specification.new do |spec|
17
17
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
18
18
  spec.require_paths = ["lib"]
19
19
 
20
+ spec.add_development_dependency "activerecord", "~> 5.2"
20
21
  spec.add_development_dependency "bundler", "1.15.2"
21
- spec.add_development_dependency "rake", "~> 10.0"
22
+ spec.add_development_dependency "byebug", "~> 10.0"
23
+ spec.add_development_dependency "pg", "~> 1.0"
22
24
  spec.add_development_dependency "rspec", "~> 3.0"
23
25
  end
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env bash
2
+
3
+ set -x
4
+ pg_dump --clean --create --format=plain --if-exists \
5
+ --no-password --no-owner --no-acl advanced_search > spec/db/pg.dump
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ abort 'This project does not use rake. See eg. bin/test'
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env bash
2
+
3
+ set -e -x
4
+ psql < spec/db/pg.dump > /dev/null
5
+ bundle exec rspec
@@ -1,6 +1,11 @@
1
1
  require "advanced_search/version"
2
- require "advanced_search/junctive"
2
+ require "advanced_search/ast/and"
3
+ require "advanced_search/ast/eq"
4
+ require "advanced_search/ast/id"
5
+ require "advanced_search/ast/lt"
6
+ require "advanced_search/ast/or"
7
+ require "advanced_search/ast/value"
8
+ require "advanced_search/sexp/s"
3
9
 
4
- module AdvancedSearch
5
- # Your code goes here...
6
- end
10
+ # We don't `require` adapters. They're going to get moved to separate
11
+ # gems so that library consumers can `require` only what they need.
@@ -0,0 +1,9 @@
1
+ module AdvancedSearch
2
+ module Adapters
3
+ module Mysql2
4
+ class Executor
5
+ # TDB
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module AdvancedSearch
2
+ module Adapters
3
+ module Mysql2
4
+ class Query
5
+ # TBD
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module AdvancedSearch
2
+ module Adapters
3
+ module Mysql2
4
+ class Visitor
5
+ # TBD
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,24 @@
1
+ require 'pg'
2
+ require 'advanced_search/adapters/pg/visitor'
3
+
4
+ module AdvancedSearch
5
+ module Adapters
6
+ module PG
7
+ class Executor
8
+ def initialize(base_query, ast, connection)
9
+ @base_query = base_query
10
+ @ast = ast
11
+ @connection = connection
12
+ end
13
+
14
+ def execute
15
+ visitor = Visitor.new(:dollars)
16
+ @ast.accept(visitor)
17
+ query = visitor.result
18
+ sql = [@base_query, query.body].join(' where ')
19
+ @connection.exec_params(sql, query.params)
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,14 @@
1
+ module AdvancedSearch
2
+ module Adapters
3
+ module PG
4
+ class Query
5
+ def initialize(body, params)
6
+ @body = body
7
+ @params = params.to_a
8
+ end
9
+
10
+ attr_reader :body, :params
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,75 @@
1
+ require 'advanced_search/adapters/pg/query'
2
+
3
+ module AdvancedSearch
4
+ module Adapters
5
+ module PG
6
+ class Visitor
7
+ def initialize(placeholder_style)
8
+ @placeholder_style = placeholder_style
9
+ @sql = []
10
+ @binds = []
11
+ end
12
+
13
+ def result
14
+ Query.new(@sql.join(' '), @binds)
15
+ end
16
+
17
+ def visit_and(node)
18
+ @sql << '('
19
+ node.edges.each_with_index do |child, i|
20
+ unless i.zero?
21
+ @sql << 'and'
22
+ end
23
+ child.accept(self)
24
+ end
25
+ @sql << ')'
26
+ end
27
+
28
+ def visit_eq(node)
29
+ node.edges[0].accept(self)
30
+ @sql << '='
31
+ node.edges[1].accept(self)
32
+ end
33
+
34
+ def visit_id(node)
35
+ @sql << node.id
36
+ end
37
+
38
+ def visit_lt(node)
39
+ node.edges[0].accept(self)
40
+ @sql << '<'
41
+ node.edges[1].accept(self)
42
+ end
43
+
44
+ def visit_or(node)
45
+ @sql << '('
46
+ node.edges.each_with_index do |child, i|
47
+ unless i.zero?
48
+ @sql << 'or'
49
+ end
50
+ child.accept(self)
51
+ end
52
+ @sql << ')'
53
+ end
54
+
55
+ def visit_value(node)
56
+ @sql << placeholder(@binds.length + 1)
57
+ @binds << node.value
58
+ end
59
+
60
+ private
61
+
62
+ def placeholder(ordinal_number)
63
+ case @placeholder_style
64
+ when :question_marks
65
+ '?'
66
+ when :dollars
67
+ format('$%d', ordinal_number)
68
+ else
69
+ raise 'Invalid placeholder style'
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,11 @@
1
+ require "advanced_search/ast/base"
2
+
3
+ module AdvancedSearch
4
+ module AST
5
+ class And < Base
6
+ def accept(visitor)
7
+ visitor.visit_and(self)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,24 @@
1
+ module AdvancedSearch
2
+ module AST
3
+ class Base
4
+ def initialize
5
+ @edges = []
6
+ end
7
+
8
+ attr_reader :edges
9
+
10
+ def add_edge(other_node)
11
+ unless other_node.is_a?(Base)
12
+ raise(
13
+ TypeError,
14
+ format(
15
+ 'Invalid AST edge. Expected AdvancedSearch::AST::Base, got %s',
16
+ other_node
17
+ )
18
+ )
19
+ end
20
+ @edges.push(other_node)
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,11 @@
1
+ require "advanced_search/ast/base"
2
+
3
+ module AdvancedSearch
4
+ module AST
5
+ class Eq < Base
6
+ def accept(visitor)
7
+ visitor.visit_eq(self)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,18 @@
1
+ require "advanced_search/ast/base"
2
+
3
+ module AdvancedSearch
4
+ module AST
5
+ # Trusted. Do not construct an `Id` directly from user input.
6
+ class Id < Base
7
+ def initialize(id)
8
+ @id = id
9
+ end
10
+
11
+ attr_reader :id
12
+
13
+ def accept(visitor)
14
+ visitor.visit_id(self)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,11 @@
1
+ require "advanced_search/ast/base"
2
+
3
+ module AdvancedSearch
4
+ module AST
5
+ class Lt < Base
6
+ def accept(visitor)
7
+ visitor.visit_lt(self)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ require "advanced_search/ast/base"
2
+
3
+ module AdvancedSearch
4
+ module AST
5
+ class Or < Base
6
+ def accept(visitor)
7
+ visitor.visit_or(self)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,17 @@
1
+ require "advanced_search/ast/base"
2
+
3
+ module AdvancedSearch
4
+ module AST
5
+ class Value < Base
6
+ def initialize(value)
7
+ @value = value
8
+ end
9
+
10
+ attr_reader :value
11
+
12
+ def accept(visitor)
13
+ visitor.visit_value(self)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,6 @@
1
+ module AdvancedSearch
2
+ # Base class for all AdvancedSearch errors.
3
+ # @api public
4
+ class Error < StandardError
5
+ end
6
+ end
@@ -0,0 +1,34 @@
1
+ require "advanced_search/error"
2
+
3
+ module AdvancedSearch
4
+ module SExp
5
+ # A nicer presentation of a `NameError`.
6
+ #
7
+ # @api public
8
+ class InvalidType < ::AdvancedSearch::Error
9
+ # @api private
10
+ def initialize(type, name_error)
11
+ @type = type
12
+ @name_error = name_error
13
+ end
14
+
15
+ # @api public
16
+ # @return String
17
+ def message
18
+ format(
19
+ 'Invalid S-expression type: %s (%s) Valid types are: %s',
20
+ @type,
21
+ @name_error.message,
22
+ valid_types.join(', ')
23
+ )
24
+ end
25
+
26
+ private
27
+
28
+ # @api private
29
+ def valid_types
30
+ AST.constants.reject { |sym| sym == :Base }.map { |sym| sym.to_s.downcase }
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,59 @@
1
+ require "advanced_search/sexp/invalid_type"
2
+
3
+ module AdvancedSearch
4
+ module SExp
5
+ # @api private
6
+ class NodeBuilder
7
+ # @api private
8
+ def initialize(type, *args)
9
+ @type = type
10
+ @args = args
11
+ end
12
+
13
+ # @api private
14
+ def build
15
+ constructor_args, child_nodes = split_args
16
+ # puts [@type, @args, constructor_arity, constructor_args, child_nodes].inspect
17
+ node = node_class.new(*constructor_args)
18
+ add_edges(node, child_nodes)
19
+ node
20
+ end
21
+
22
+ private
23
+
24
+ # @api private
25
+ def add_edges(node, children)
26
+ children.each do |i|
27
+ node.add_edge(i)
28
+ end
29
+ end
30
+
31
+ # @api private
32
+ def constructor_arity
33
+ node_class.allocate.method(:initialize).arity
34
+ end
35
+
36
+ # @api private
37
+ def node_class
38
+ @_node_class ||= ::Object.const_get(node_class_path)
39
+ rescue ::NameError => e
40
+ raise InvalidType.new(@type, e)
41
+ end
42
+
43
+ # @api private
44
+ # @return String - absolute constant path
45
+ def node_class_path
46
+ format('::AdvancedSearch::AST::%s', @type.to_s.capitalize)
47
+ end
48
+
49
+ # @api private
50
+ def split_args
51
+ c_arity = constructor_arity
52
+ [
53
+ @args[0, c_arity],
54
+ @args[c_arity..-1]
55
+ ]
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,44 @@
1
+ require "advanced_search/sexp/node_builder"
2
+
3
+ module AdvancedSearch
4
+ module SExp
5
+ # S-expressions are a concise way to instantiate AST nodes.
6
+ #
7
+ # ```
8
+ # # This ..
9
+ # eq = ::AdvancedSearch::Nodes::Lt.new
10
+ # eq.add_edge(::AdvancedSearch::Nodes::Id.new(:age))
11
+ # eq.add_edge(::AdvancedSearch::Nodes::Value.new(40))
12
+ #
13
+ # # .. becomes
14
+ # include ::AdvancedSearch::SExp::S
15
+ # eq = s(:lt, s(:id, :age), s(:value, 40))
16
+ # ```
17
+ #
18
+ # You don't have to use S-expressions to use AdvancedSearch, but they are
19
+ # convenient.
20
+ #
21
+ # @api public
22
+ module S
23
+ # A concise way to instantiate AST nodes.
24
+ #
25
+ # ```
26
+ # include ::AdvancedSearch::SExp::S
27
+ # eq = s(:lt, s(:id, :age), s(:value, 40))
28
+ # ```
29
+ #
30
+ # As this method is an important part of the AdvancedSearch API, special
31
+ # care is taken to produce helpful error messages when given incorrect
32
+ # arguments.
33
+ #
34
+ # @api public
35
+ # @param type [String, Symbol] - The basename of a descendent of AST::Base.
36
+ # E.g. to instantiate an `AST:Eq`, use `'eq'` or `:eq`.
37
+ # @param *args - Arguments for the AST node constructor first. Subsequent
38
+ # arguments become child nodes.
39
+ def s(type, *args)
40
+ NodeBuilder.new(type, *args).build
41
+ end
42
+ end
43
+ end
44
+ end
@@ -1,3 +1,7 @@
1
1
  module AdvancedSearch
2
- VERSION = "0.0.1"
2
+ # @api public
3
+ # @added 0.0.2
4
+ def self.gem_version
5
+ ::Gem::Version.new('0.0.2')
6
+ end
3
7
  end
metadata CHANGED
@@ -1,15 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: advanced_search
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jared Beck
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2018-10-04 00:00:00.000000000 Z
11
+ date: 2018-10-30 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activerecord
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '5.2'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '5.2'
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: bundler
15
29
  requirement: !ruby/object:Gem::Requirement
@@ -25,7 +39,7 @@ dependencies:
25
39
  - !ruby/object:Gem::Version
26
40
  version: 1.15.2
27
41
  - !ruby/object:Gem::Dependency
28
- name: rake
42
+ name: byebug
29
43
  requirement: !ruby/object:Gem::Requirement
30
44
  requirements:
31
45
  - - "~>"
@@ -38,6 +52,20 @@ dependencies:
38
52
  - - "~>"
39
53
  - !ruby/object:Gem::Version
40
54
  version: '10.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: pg
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.0'
41
69
  - !ruby/object:Gem::Dependency
42
70
  name: rspec
43
71
  requirement: !ruby/object:Gem::Requirement
@@ -65,15 +93,31 @@ files:
65
93
  - CHANGELOG.md
66
94
  - CODE_OF_CONDUCT.md
67
95
  - Gemfile
68
- - Gemfile.lock
69
96
  - LICENSE
70
97
  - README.md
71
- - Rakefile
72
98
  - advanced_search.gemspec
73
99
  - bin/console
74
- - bin/setup
100
+ - bin/db/dump.sh
101
+ - bin/rake
102
+ - bin/test.sh
75
103
  - lib/advanced_search.rb
76
- - lib/advanced_search/junctive.rb
104
+ - lib/advanced_search/adapters/mysql2/executor.rb
105
+ - lib/advanced_search/adapters/mysql2/query.rb
106
+ - lib/advanced_search/adapters/mysql2/visitor.rb
107
+ - lib/advanced_search/adapters/pg/executor.rb
108
+ - lib/advanced_search/adapters/pg/query.rb
109
+ - lib/advanced_search/adapters/pg/visitor.rb
110
+ - lib/advanced_search/ast/and.rb
111
+ - lib/advanced_search/ast/base.rb
112
+ - lib/advanced_search/ast/eq.rb
113
+ - lib/advanced_search/ast/id.rb
114
+ - lib/advanced_search/ast/lt.rb
115
+ - lib/advanced_search/ast/or.rb
116
+ - lib/advanced_search/ast/value.rb
117
+ - lib/advanced_search/error.rb
118
+ - lib/advanced_search/sexp/invalid_type.rb
119
+ - lib/advanced_search/sexp/node_builder.rb
120
+ - lib/advanced_search/sexp/s.rb
77
121
  - lib/advanced_search/version.rb
78
122
  homepage: https://github.com/jaredbeck/advanced_search
79
123
  licenses:
@@ -1,35 +0,0 @@
1
- PATH
2
- remote: .
3
- specs:
4
- advanced_search (0.0.1)
5
-
6
- GEM
7
- remote: https://rubygems.org/
8
- specs:
9
- diff-lcs (1.3)
10
- rake (10.5.0)
11
- rspec (3.8.0)
12
- rspec-core (~> 3.8.0)
13
- rspec-expectations (~> 3.8.0)
14
- rspec-mocks (~> 3.8.0)
15
- rspec-core (3.8.0)
16
- rspec-support (~> 3.8.0)
17
- rspec-expectations (3.8.1)
18
- diff-lcs (>= 1.2.0, < 2.0)
19
- rspec-support (~> 3.8.0)
20
- rspec-mocks (3.8.0)
21
- diff-lcs (>= 1.2.0, < 2.0)
22
- rspec-support (~> 3.8.0)
23
- rspec-support (3.8.0)
24
-
25
- PLATFORMS
26
- ruby
27
-
28
- DEPENDENCIES
29
- advanced_search!
30
- bundler (= 1.15.2)
31
- rake (~> 10.0)
32
- rspec (~> 3.0)
33
-
34
- BUNDLED WITH
35
- 1.15.2
data/Rakefile DELETED
@@ -1,6 +0,0 @@
1
- require "bundler/gem_tasks"
2
- require "rspec/core/rake_task"
3
-
4
- RSpec::Core::RakeTask.new(:spec)
5
-
6
- task :default => :spec
data/bin/setup DELETED
@@ -1,8 +0,0 @@
1
- #!/usr/bin/env bash
2
- set -euo pipefail
3
- IFS=$'\n\t'
4
- set -vx
5
-
6
- bundle install
7
-
8
- # Do any other automated setup that you need to do here
@@ -1,218 +0,0 @@
1
- # bin/console
2
- # irb(main):001:0> AdvancedSearch::Junctive::Client.new(:or).perform(age_lt: 40, ssn_eq: '123-45-6789')
3
- # InsecureSql -> age < 40 or ssn = 123-45-6789
4
- # BoundSql -> ["age < $1 or ssn = $2", [40, "123-45-6789"]]
5
- # BananaSql -> 🍌 < 🍌 or 🍌 = 🍌
6
- # => nil
7
-
8
- module AdvancedSearch
9
- module Junctive
10
- module Nodes
11
- class Base
12
- def initialize
13
- @edges = []
14
- end
15
-
16
- def add_edge(other_node)
17
- @edges.push(other_node)
18
- end
19
- end
20
-
21
- class And < Base
22
- def accept(visitor)
23
- @edges.each_with_index do |node, i|
24
- unless i.zero?
25
- visitor.visit_and
26
- end
27
- node.accept(visitor)
28
- end
29
- end
30
- end
31
-
32
- class Eq < Base
33
- def accept(visitor)
34
- @edges[0].accept(visitor)
35
- visitor.visit_eq
36
- @edges[1].accept(visitor)
37
- end
38
- end
39
-
40
- class Id < Base
41
- def initialize(id)
42
- @id = id
43
- end
44
-
45
- attr_reader :id
46
-
47
- def accept(visitor)
48
- visitor.visit_id(self)
49
- end
50
- end
51
-
52
- class Lt < Base
53
- def accept(visitor)
54
- @edges[0].accept(visitor)
55
- visitor.visit_lt
56
- @edges[1].accept(visitor)
57
- end
58
- end
59
-
60
- class Or < Base
61
- def accept(visitor)
62
- @edges.each_with_index do |node, i|
63
- unless i.zero?
64
- visitor.visit_or
65
- end
66
- node.accept(visitor)
67
- end
68
- end
69
- end
70
-
71
- class Value < Base
72
- def initialize(value)
73
- @value = value
74
- end
75
-
76
- attr_reader :value
77
-
78
- def accept(visitor)
79
- visitor.visit_value(self)
80
- end
81
- end
82
- end
83
-
84
- module Visitors
85
- class Sql
86
- def initialize
87
- @sql = []
88
- end
89
-
90
- def visit_and
91
- @sql << 'and'
92
- end
93
-
94
- def visit_eq
95
- @sql << '='
96
- end
97
-
98
- def visit_lt
99
- @sql << '<'
100
- end
101
-
102
- def visit_or
103
- @sql << 'or'
104
- end
105
- end
106
-
107
- class BoundSql < Sql
108
- def initialize
109
- super
110
- @binds = []
111
- end
112
-
113
- def result
114
- [@sql.join(' '), @binds]
115
- end
116
-
117
- def visit_id(node)
118
- @sql << node.id
119
- end
120
-
121
- def visit_value(node)
122
- @sql << format('$%d', @binds.length + 1)
123
- @binds << node.value
124
- end
125
- end
126
-
127
- # Vulnerable to SQL-injection
128
- class InsecureSql < Sql
129
- def initialize
130
- @sql = []
131
- end
132
-
133
- def result
134
- @sql.join(' ')
135
- end
136
-
137
- def visit_id(node)
138
- @sql << node.id
139
- end
140
-
141
- def visit_value(node)
142
- @sql << node.value
143
- end
144
- end
145
-
146
- class BananaSql < Sql
147
- def initialize
148
- @sql = []
149
- end
150
-
151
- def result
152
- @sql.join(' ')
153
- end
154
-
155
- def visit_id(_node)
156
- @sql << '🍌'
157
- end
158
-
159
- def visit_value(_node)
160
- @sql << '🍌'
161
- end
162
- end
163
- end
164
-
165
- class Client
166
- # The head of the tree is either a conjunction or disjunction, depending
167
- # on `operator`.
168
- def initialize(operator)
169
- @head = get_head(operator)
170
- end
171
-
172
- def perform(params)
173
- # Build a graph (tree) by dynamically `send`ing the parameter name.
174
- params.each { |k, v| send(k, v) }
175
-
176
- # Now, we have a graph (tree) which we can iterate (DFS) using whichever
177
- # visitor we feel like using.
178
- traverse(Visitors::InsecureSql)
179
- traverse(Visitors::BoundSql)
180
- traverse(Visitors::BananaSql)
181
- end
182
-
183
- private
184
-
185
- def age_lt(v)
186
- eq = Nodes::Lt.new
187
- eq.add_edge(Nodes::Id.new(:age))
188
- eq.add_edge(Nodes::Value.new(v))
189
- @head.add_edge(eq)
190
- end
191
-
192
- def get_head(operator)
193
- case operator
194
- when :and
195
- Nodes::And.new
196
- when :or
197
- Nodes::Or.new
198
- else
199
- raise 'invalid operator'
200
- end
201
- end
202
-
203
- def ssn_eq(v)
204
- eq = Nodes::Eq.new
205
- eq.add_edge(Nodes::Id.new(:ssn))
206
- eq.add_edge(Nodes::Value.new(v))
207
- @head.add_edge(eq)
208
- end
209
-
210
- def traverse(visitor_class)
211
- visitor = visitor_class.new
212
- @head.accept(visitor)
213
- puts format('%20s -> %s', visitor_class.name.split('::').last, visitor.result)
214
- nil
215
- end
216
- end
217
- end
218
- end