advanced_search 0.0.1 → 0.0.2

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 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