todoist_querynaut 0.1.0

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: c812ff2de231d377f592d944c80e8e090803ce39
4
+ data.tar.gz: 54f155642e5e2f205eb8b18011495568ab6efb7c
5
+ SHA512:
6
+ metadata.gz: 5b9f67cdd4ad545a160c8094bd3613b996780d4c55580c2e3225d44aa938af98f08b62a0b078f938a2aa2b9c1fde6119c1d7ee1164914495a78f61f45981ddec
7
+ data.tar.gz: ab61e52a42a093363e88ca21f7ed1d26838f36401feae953e0fc8cc73ffb3cedfb4d660d284f8c35fbdafe28906e030502f41af1d5efea924fda9b5d69ebb40d
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2017 Stefan Siegl
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,47 @@
1
+ # TodoistQuerynaut
2
+
3
+ Todoist Querynaut is a Ruby Gem that reimplements the [filter query language](https://support.todoist.com/hc/en-us/articles/205248842-Filters)
4
+ of Todoist.
5
+
6
+ [Todoist](https://todoist.com) has a powerful feature named filters,
7
+ yet they are implemented on client side (i.e. within their web application).
8
+ It even has a REST API that allows to do searches, yet they are not as powerful,
9
+ e.g. they don't allow for boolean operator logic.
10
+
11
+ This is where Todoist Querynaut kicks in, it has a parser for the query
12
+ language and does one or more calls to the REST API to collect the correct set
13
+ of result items.
14
+
15
+ Querynaut isn't yet fully fledged, some search capabilities are missing
16
+
17
+ * search for items within sub-projects
18
+ * filtering based on exact due dates
19
+ * filtering based on creation date
20
+ * "no labels" query
21
+
22
+ ## Installation
23
+
24
+ Add this line to your application's Gemfile
25
+
26
+ ```ruby
27
+ gem 'todoist_querynaut'
28
+ ```
29
+
30
+ And then execute:
31
+
32
+ $ bundle install
33
+
34
+ ## Usage
35
+
36
+ ```ruby
37
+ querynaut = TodoistQuerynaut::Client.new(Todoist::Client.new("your_api_token"))
38
+ result = @client.run("(overdue | today) & #work")
39
+ ```
40
+
41
+ ## Contributing
42
+
43
+ 1. Fork it ( https://github.com/stesie/todoist_querynaut/fork )
44
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
45
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
46
+ 4. Push to the branch (`git push origin my-new-feature`)
47
+ 5. Create a new Pull Request
@@ -0,0 +1,9 @@
1
+ require 'treetop'
2
+ require 'todoist'
3
+
4
+ module TodoistQuerynaut; end
5
+
6
+ require 'todoist_querynaut/client'
7
+ require 'todoist_querynaut/parser'
8
+ require 'todoist_querynaut/project_not_found_error'
9
+ require 'todoist_querynaut/todoist_query/node_extensions'
@@ -0,0 +1,36 @@
1
+ module TodoistQuerynaut
2
+ class Client
3
+ def initialize(todoist)
4
+ @todoist = todoist
5
+ end
6
+
7
+ def run(query)
8
+ TodoistQuerynaut::Parser.parse(query).run_query self
9
+ end
10
+
11
+ def search(query)
12
+ # Todoist doesn't always set "query" on the results, hence we cannot
13
+ # search(value)[value] here, as ruby-todoist-api Gem then assigns "nil"
14
+ # as the hash's key. Instead we do .first.last, to get the value of the
15
+ # first hash entry
16
+ @todoist.query.search(query).first.last.data
17
+ end
18
+
19
+ def all_items
20
+ search "view all"
21
+ end
22
+
23
+ def project_name_to_id(name)
24
+ projects = all_projects.select{|p| p["name"].casecmp(name) == 0}
25
+ raise ProjectNotFoundError if projects.empty?
26
+ projects.first["id"].to_i
27
+ end
28
+
29
+ private
30
+
31
+ def all_projects()
32
+ @projects ||= @todoist.projects.retrieve(["projects"])["Projects"]
33
+ end
34
+ end
35
+ end
36
+
@@ -0,0 +1,35 @@
1
+ module TodoistQuerynaut
2
+ class Parser
3
+ Treetop.load(File.join(File.dirname(__FILE__), 'todoist_query_parser.treetop'))
4
+ @@parser = TodoistQueryParser.new
5
+
6
+ def self.parse(data)
7
+ tree = @@parser.parse(data)
8
+
9
+ if tree.nil?
10
+ raise Exception, "Parse error at offset: #{@@parser.index}"
11
+ end
12
+
13
+ #return tree
14
+ return self.clean_tree(tree)
15
+ end
16
+
17
+ private
18
+
19
+ def self.clean_tree(node)
20
+ if node.class.name == "Treetop::Runtime::SyntaxNode"
21
+ if node.elements.nil? || node.elements.length == 0
22
+ return node.empty? ? [] : node
23
+ else
24
+ return node.elements.map{|n| self.clean_tree n}.flatten(1)
25
+ end
26
+ else
27
+ if !node.elements.nil?
28
+ node.elements.replace node.elements.map{|n| self.clean_tree n}.flatten(1)
29
+ return node.children[0] if node.is_a?(TodoistQuery::SetExpressionNode) && node.sole?
30
+ end
31
+ return node
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,2 @@
1
+ class ProjectNotFoundError < RuntimeError
2
+ end
@@ -0,0 +1,72 @@
1
+ module TodoistQuerynaut
2
+ module TodoistQuery
3
+ class SetExpressionNode < Treetop::Runtime::SyntaxNode
4
+ def children
5
+ elements.reject{|a| a.class.name == 'Treetop::Runtime::SyntaxNode'}
6
+ end
7
+
8
+ def sole?
9
+ children.size == 1
10
+ end
11
+ end
12
+
13
+ class Union < SetExpressionNode
14
+ def run_query(todoist)
15
+ children.inject [] {|acc,child| acc | (child.run_query todoist)}
16
+ end
17
+ end
18
+
19
+ class Intersection < SetExpressionNode
20
+ def run_query(todoist)
21
+ acc = children[0].run_query todoist
22
+ children.drop(1).inject(acc) {|acc,child| acc & (child.run_query todoist)}
23
+ end
24
+ end
25
+
26
+ class LiteralQuery < Treetop::Runtime::SyntaxNode
27
+ def value
28
+ text_value
29
+ end
30
+
31
+ def run_query(todoist)
32
+ todoist.search(value)
33
+ end
34
+ end
35
+
36
+ class NDaysQuery < Treetop::Runtime::SyntaxNode
37
+ end
38
+
39
+ class PriorityQuery < Treetop::Runtime::SyntaxNode
40
+ def value
41
+ text_value[-1].to_i
42
+ end
43
+
44
+ def run_query(todoist)
45
+ todoist.search "p#{value}"
46
+ end
47
+ end
48
+
49
+ class ProjectNameQuery < Treetop::Runtime::SyntaxNode
50
+ def value
51
+ text_value[2..-1]
52
+ end
53
+
54
+ def run_query(todoist)
55
+ project_id = todoist.project_name_to_id value
56
+ todoist.all_items.select{|item| item["project_id"] == project_id}
57
+ end
58
+ end
59
+
60
+ class LabelQuery < Treetop::Runtime::SyntaxNode
61
+ end
62
+
63
+ class NoLabelsQuery < Treetop::Runtime::SyntaxNode
64
+ end
65
+
66
+ class NegatedQuery < Treetop::Runtime::SyntaxNode
67
+ def run_query(todoist)
68
+ todoist.all_items - query.run_query(todoist)
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,55 @@
1
+ module TodoistQuerynaut
2
+ grammar TodoistQuery
3
+ rule union
4
+ intersection ( '|' intersection )* <Union>
5
+ end
6
+
7
+ rule intersection
8
+ primary ( '&' primary )* <Intersection>
9
+ end
10
+
11
+ rule primary
12
+ space? ( '(' union ')' / query_part ) space?
13
+ end
14
+
15
+ rule query_part
16
+ negated_part / literal_query / n_days_query / priority_query / project_name_query / label_query / no_labels_query
17
+ end
18
+
19
+ rule negated_part
20
+ '!' query:query_part <NegatedQuery>
21
+ end
22
+
23
+ rule literal_query
24
+ 'over due' <LiteralQuery> / 'overdue' <LiteralQuery> / 'today' <LiteralQuery> / 'tomorrow' <LiteralQuery> / 'view all' <LiteralQuery>
25
+ end
26
+
27
+ rule n_days_query
28
+ [0-9]+ space "days" <NDaysQuery> {
29
+ def value
30
+ text_value.to_i
31
+ end
32
+ }
33
+ end
34
+
35
+ rule priority_query
36
+ ("priority" space / "p") [1-4] <PriorityQuery>
37
+ end
38
+
39
+ rule project_name_query
40
+ 'p:' [\w]+ <ProjectNameQuery>
41
+ end
42
+
43
+ rule label_query
44
+ '@' [\w]+ <LiteralQuery>
45
+ end
46
+
47
+ rule no_labels_query
48
+ 'no labels' <NoLabelsQuery>
49
+ end
50
+
51
+ rule space
52
+ [\s]+
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,68 @@
1
+ require "spec_helper"
2
+
3
+ describe TodoistQuerynaut::Client do
4
+ describe "#search" do
5
+ it "delegates search query to todoist api and returns result" do
6
+ todoist_api = double()
7
+ allow(todoist_api).to receive(:query) {
8
+ query_obj = double()
9
+ allow(query_obj).to receive(:search) { |query|
10
+ { query => Todoist::Result.new({ "query" => "foo", "data" => [ "content_here" ] }) }
11
+ }
12
+ query_obj
13
+ }
14
+
15
+ result = TodoistQuerynaut::Client.new(todoist_api).search("foobar")
16
+ expect(result).to eq(["content_here"])
17
+ end
18
+ end
19
+
20
+ describe "#all_items" do
21
+ it "should run a 'view all' query" do
22
+ stub_request(:post, "https://todoist.com/API/v6/query").
23
+ with(:body => {"queries" => "[\"view all\"]", "token" => "some_token"}).
24
+ to_return(:status => 200, :body => json_response_raw("query_view_all"), :headers => {})
25
+ result = TodoistQuerynaut::Client.new(Todoist::Client.new("some_token")).all_items
26
+
27
+ expect(result.size).to eq(4)
28
+ end
29
+ end
30
+
31
+ describe "#project_name_to_id" do
32
+ before :each do
33
+ stub_request(:post, "https://todoist.com/API/v6/sync").
34
+ with(:body => { "seq_no" => "0", "seq_no_global" => "0", "resource_types" => '["projects"]', "token" => "some_token" }).
35
+ to_return(:status => 200, :body => json_response_raw("sync_projects_all"), :headers => {})
36
+ @client = TodoistQuerynaut::Client.new(Todoist::Client.new("some_token"))
37
+ end
38
+
39
+ it "should resolve 'Someday Maybe' to 185594700" do
40
+ result = @client.project_name_to_id("Someday Maybe")
41
+ expect(result).to eq(185594700)
42
+ end
43
+
44
+ it "should resolve project names without case-sensitivity" do
45
+ result = @client.project_name_to_id("SomeDAY MayBE")
46
+ expect(result).to eq(185594700)
47
+ end
48
+
49
+ it "should raise ProjectNotFoundError for 'No Such Project'" do
50
+ expect{ @client.project_name_to_id("No Such Project") }.to raise_error(ProjectNotFoundError)
51
+ end
52
+ end
53
+
54
+ describe "#run" do
55
+ before :each do
56
+ stub_request(:post, "https://todoist.com/API/v6/query").
57
+ with(:body => {"queries" => "[\"view all\"]", "token" => "some_token"}).
58
+ to_return(:status => 200, :body => json_response_raw("query_view_all"), :headers => {})
59
+ @client = TodoistQuerynaut::Client.new(Todoist::Client.new("some_token"))
60
+ end
61
+
62
+ it "should parse the query, execute it and return the results" do
63
+ result = @client.run("view all")
64
+ expect(result.size).to eq(4)
65
+ end
66
+ end
67
+
68
+ end
@@ -0,0 +1,207 @@
1
+ require "spec_helper"
2
+
3
+ describe "#parse" do
4
+ describe "query literals" do
5
+ it "should parse 'today'" do
6
+ tree = TodoistQuerynaut::Parser.parse("today")
7
+ expect(tree).to be_a(TodoistQuerynaut::TodoistQuery::LiteralQuery)
8
+ end
9
+
10
+ it "should ignore trailing whitespace" do
11
+ tree = TodoistQuerynaut::Parser.parse("today ")
12
+ expect(tree).to be_a(TodoistQuerynaut::TodoistQuery::LiteralQuery)
13
+ end
14
+
15
+ it "should ignore leading whitespace" do
16
+ tree = TodoistQuerynaut::Parser.parse(" today")
17
+ expect(tree).to be_a(TodoistQuerynaut::TodoistQuery::LiteralQuery)
18
+ end
19
+
20
+ it "should parse 'tomorrow'" do
21
+ tree = TodoistQuerynaut::Parser.parse("tomorrow")
22
+ expect(tree).to be_a(TodoistQuerynaut::TodoistQuery::LiteralQuery)
23
+ end
24
+
25
+ it "should parse 'overdue'" do
26
+ tree = TodoistQuerynaut::Parser.parse("overdue")
27
+ expect(tree).to be_a(TodoistQuerynaut::TodoistQuery::LiteralQuery)
28
+ end
29
+
30
+ it "should parse 'over due'" do
31
+ tree = TodoistQuerynaut::Parser.parse("over due")
32
+ expect(tree).to be_a(TodoistQuerynaut::TodoistQuery::LiteralQuery)
33
+ end
34
+
35
+ it "should parse 'view all'" do
36
+ tree = TodoistQuerynaut::Parser.parse("view all")
37
+ expect(tree).to be_a(TodoistQuerynaut::TodoistQuery::LiteralQuery)
38
+ end
39
+ end
40
+
41
+ describe "negated queries" do
42
+ it "should parse '!today'" do
43
+ tree = TodoistQuerynaut::Parser.parse("!today")
44
+ expect(tree).to be_a(TodoistQuerynaut::TodoistQuery::NegatedQuery)
45
+ expect(tree.query).to be_a(TodoistQuerynaut::TodoistQuery::LiteralQuery)
46
+ end
47
+ end
48
+
49
+ describe "n-days queries" do
50
+ it "should parse '4 days'" do
51
+ tree = TodoistQuerynaut::Parser.parse("4 days")
52
+ expect(tree).to be_a(TodoistQuerynaut::TodoistQuery::NDaysQuery)
53
+ expect(tree.value).to eq(4)
54
+ end
55
+
56
+ it "should parse '23 days'" do
57
+ tree = TodoistQuerynaut::Parser.parse("23 days")
58
+ expect(tree).to be_a(TodoistQuerynaut::TodoistQuery::NDaysQuery)
59
+ expect(tree.value).to eq(23)
60
+ end
61
+
62
+ it "should parse '42 days'" do
63
+ tree = TodoistQuerynaut::Parser.parse("42 days")
64
+ expect(tree).to be_a(TodoistQuerynaut::TodoistQuery::NDaysQuery)
65
+ expect(tree.value).to eq(42)
66
+ end
67
+ end
68
+
69
+ describe "priority queries" do
70
+ it "should parse 'p1'" do
71
+ tree = TodoistQuerynaut::Parser.parse("p1")
72
+ expect(tree).to be_a(TodoistQuerynaut::TodoistQuery::PriorityQuery)
73
+ expect(tree.value).to eq(1)
74
+ end
75
+
76
+ it "should parse 'priority 3'" do
77
+ tree = TodoistQuerynaut::Parser.parse("priority 3")
78
+ expect(tree).to be_a(TodoistQuerynaut::TodoistQuery::PriorityQuery)
79
+ expect(tree.value).to eq(3)
80
+ end
81
+
82
+ it "should parse 'priority 2'" do
83
+ tree = TodoistQuerynaut::Parser.parse("priority 2")
84
+ expect(tree).to be_a(TodoistQuerynaut::TodoistQuery::PriorityQuery)
85
+ expect(tree.value).to eq(2)
86
+ end
87
+ end
88
+
89
+ describe "project name queries" do
90
+ it "should parse 'p:foo'" do
91
+ tree = TodoistQuerynaut::Parser.parse("p:foo")
92
+ expect(tree).to be_a(TodoistQuerynaut::TodoistQuery::ProjectNameQuery)
93
+ expect(tree.value).to eq("foo")
94
+ end
95
+
96
+ it "should parse 'p:some_long_project23'" do
97
+ tree = TodoistQuerynaut::Parser.parse("p:some_long_project23")
98
+ expect(tree).to be_a(TodoistQuerynaut::TodoistQuery::ProjectNameQuery)
99
+ expect(tree.value).to eq("some_long_project23")
100
+ end
101
+ end
102
+
103
+ describe "label queries" do
104
+ it "should parse '@foo'" do
105
+ tree = TodoistQuerynaut::Parser.parse("@foo")
106
+ expect(tree).to be_a(TodoistQuerynaut::TodoistQuery::LiteralQuery)
107
+ expect(tree.value).to eq("@foo")
108
+ end
109
+
110
+ it "should parse '@rspec'" do
111
+ tree = TodoistQuerynaut::Parser.parse("@rspec")
112
+ expect(tree).to be_a(TodoistQuerynaut::TodoistQuery::LiteralQuery)
113
+ expect(tree.value).to eq("@rspec")
114
+ end
115
+
116
+ it "should parse 'no labels'" do
117
+ tree = TodoistQuerynaut::Parser.parse("no labels")
118
+ expect(tree).to be_a(TodoistQuerynaut::TodoistQuery::NoLabelsQuery)
119
+ end
120
+ end
121
+
122
+ describe "logical operations" do
123
+ describe "intersections" do
124
+ it "should support intersections of two labels'" do
125
+ tree = TodoistQuerynaut::Parser.parse("@foo & @bar")
126
+ expect(tree).to be_a(TodoistQuerynaut::TodoistQuery::Intersection)
127
+ expect(tree.children[0]).to be_a(TodoistQuerynaut::TodoistQuery::LiteralQuery)
128
+ expect(tree.children[0].value).to eq("@foo")
129
+ expect(tree.children[1]).to be_a(TodoistQuerynaut::TodoistQuery::LiteralQuery)
130
+ expect(tree.children[1].value).to eq("@bar")
131
+ end
132
+
133
+ it "should support intersections of two different query parts'" do
134
+ tree = TodoistQuerynaut::Parser.parse("p4 & today")
135
+ expect(tree).to be_a(TodoistQuerynaut::TodoistQuery::Intersection)
136
+ expect(tree.children.size).to eq(2)
137
+ expect(tree.children[0]).to be_a(TodoistQuerynaut::TodoistQuery::PriorityQuery)
138
+ expect(tree.children[0].value).to eq(4)
139
+ expect(tree.children[1]).to be_a(TodoistQuerynaut::TodoistQuery::LiteralQuery)
140
+ expect(tree.children[1].value).to eq("today")
141
+ end
142
+
143
+
144
+ it "should support intersections of multiple parts" do
145
+ tree = TodoistQuerynaut::Parser.parse("@foo & @bar & @here & @there")
146
+ expect(tree).to be_a(TodoistQuerynaut::TodoistQuery::Intersection)
147
+ expect(tree.children.size).to eq(4)
148
+ tree.children.each { |node| expect(node).to be_a(TodoistQuerynaut::TodoistQuery::LiteralQuery) }
149
+ end
150
+ end
151
+
152
+ describe "unions" do
153
+ it "should support unions of two labels'" do
154
+ tree = TodoistQuerynaut::Parser.parse("@foo | @bar")
155
+ expect(tree).to be_a(TodoistQuerynaut::TodoistQuery::Union)
156
+ expect(tree.children[0]).to be_a(TodoistQuerynaut::TodoistQuery::LiteralQuery)
157
+ expect(tree.children[0].value).to eq("@foo")
158
+ expect(tree.children[1]).to be_a(TodoistQuerynaut::TodoistQuery::LiteralQuery)
159
+ expect(tree.children[1].value).to eq("@bar")
160
+ end
161
+
162
+ it "should support unions of two different query parts'" do
163
+ tree = TodoistQuerynaut::Parser.parse("p4 | today")
164
+ expect(tree).to be_a(TodoistQuerynaut::TodoistQuery::Union)
165
+ expect(tree.children.size).to eq(2)
166
+ expect(tree.children[0]).to be_a(TodoistQuerynaut::TodoistQuery::PriorityQuery)
167
+ expect(tree.children[0].value).to eq(4)
168
+ expect(tree.children[1]).to be_a(TodoistQuerynaut::TodoistQuery::LiteralQuery)
169
+ expect(tree.children[1].value).to eq("today")
170
+ end
171
+
172
+ it "should support unions of multiple parts" do
173
+ tree = TodoistQuerynaut::Parser.parse("@foo | @bar | @here | @there")
174
+ expect(tree).to be_a(TodoistQuerynaut::TodoistQuery::Union)
175
+ expect(tree.children.size).to eq(4)
176
+ tree.children.each { |node| expect(node).to be_a(TodoistQuerynaut::TodoistQuery::LiteralQuery) }
177
+ end
178
+ end
179
+
180
+ describe "operator precedence" do
181
+ it "should give higher precedence to intersection" do
182
+ tree = TodoistQuerynaut::Parser.parse("p1 & p1 | p4")
183
+ expect(tree).to be_a(TodoistQuerynaut::TodoistQuery::Union)
184
+ expect(tree.children[0]).to be_a(TodoistQuerynaut::TodoistQuery::Intersection)
185
+ end
186
+
187
+ it "should give higher precedence to intersection (2)" do
188
+ tree = TodoistQuerynaut::Parser.parse("p4 | p1 & p1")
189
+ expect(tree).to be_a(TodoistQuerynaut::TodoistQuery::Union)
190
+ expect(tree.children[1]).to be_a(TodoistQuerynaut::TodoistQuery::Intersection)
191
+ end
192
+
193
+ it "should give even higher precedence to parentheses" do
194
+ tree = TodoistQuerynaut::Parser.parse("p1 & (p1 | p4)")
195
+ expect(tree).to be_a(TodoistQuerynaut::TodoistQuery::Intersection)
196
+ expect(tree.children[1]).to be_a(TodoistQuerynaut::TodoistQuery::Union)
197
+ end
198
+
199
+ it "should give even higher precedence to parentheses (2)" do
200
+ tree = TodoistQuerynaut::Parser.parse("(p1 | p4) & p2")
201
+ expect(tree).to be_a(TodoistQuerynaut::TodoistQuery::Intersection)
202
+ expect(tree.children[0]).to be_a(TodoistQuerynaut::TodoistQuery::Union)
203
+ end
204
+ end
205
+
206
+ end
207
+ end
@@ -0,0 +1,37 @@
1
+ require "spec_helper"
2
+
3
+ describe TodoistQuerynaut::TodoistQuery::Intersection do
4
+ describe "#run_query" do
5
+ it "should return items of 'today' for 'today & today'" do
6
+ intersection_query = TodoistQuerynaut::TodoistQuery::Intersection.new("today&today", 0...13, [
7
+ TodoistQuerynaut::TodoistQuery::LiteralQuery.new("today", 0...5),
8
+ TodoistQuerynaut::TodoistQuery::LiteralQuery.new("today", 0...5)
9
+ ])
10
+
11
+ stub_request(:post, "https://todoist.com/API/v6/query").
12
+ with(:body => {"queries" => "[\"today\"]", "token" => "some_token"}).
13
+ to_return(:status => 200, :body => json_response_raw("query_today"), :headers => {})
14
+ result = intersection_query.run_query TodoistQuerynaut::Client.new(Todoist::Client.new("some_token"))
15
+
16
+ expect(result.size).to eq(1)
17
+ end
18
+
19
+ it "should run both queries and return the intersection" do
20
+ intersection_query = TodoistQuerynaut::TodoistQuery::Intersection.new("overdue & overdue_one", 0...21, [
21
+ TodoistQuerynaut::TodoistQuery::LiteralQuery.new("overdue", 0...7),
22
+ TodoistQuerynaut::TodoistQuery::LiteralQuery.new("overdue_one", 0...11)
23
+ ])
24
+
25
+ stub_request(:post, "https://todoist.com/API/v6/query").
26
+ with(:body => {"queries" => "[\"overdue_one\"]", "token" => "some_token"}).
27
+ to_return(:status => 200, :body => json_response_raw("query_overdue_one"), :headers => {})
28
+ stub_request(:post, "https://todoist.com/API/v6/query").
29
+ with(:body => {"queries" => "[\"overdue\"]", "token" => "some_token"}).
30
+ to_return(:status => 200, :body => json_response_raw("query_overdue"), :headers => {})
31
+ result = intersection_query.run_query TodoistQuerynaut::Client.new(Todoist::Client.new("some_token"))
32
+
33
+ expect(result.size).to eq(1)
34
+ expect(result[0]["content"]).to eq("overdue_two")
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,42 @@
1
+ require "spec_helper"
2
+
3
+ describe TodoistQuerynaut::TodoistQuery::LiteralQuery do
4
+ describe "#value" do
5
+ it "should return the literal query string 'today'" do
6
+ literal_query = TodoistQuerynaut::TodoistQuery::LiteralQuery.new("today", 0...5)
7
+ expect(literal_query.value).to eq("today")
8
+ end
9
+
10
+ it "should return the literal query string 'overdue'" do
11
+ literal_query = TodoistQuerynaut::TodoistQuery::LiteralQuery.new("overdue", 0...7)
12
+ expect(literal_query.value).to eq("overdue")
13
+ end
14
+ end
15
+
16
+ describe "#run_query" do
17
+ it "should run a 'today' query" do
18
+ literal_query = TodoistQuerynaut::TodoistQuery::LiteralQuery.new("today", 0...5)
19
+
20
+ stub_request(:post, "https://todoist.com/API/v6/query").
21
+ with(:body => {"queries" => "[\"today\"]", "token" => "some_token"}).
22
+ to_return(:status => 200, :body => json_response_raw("query_today"), :headers => {})
23
+ result = literal_query.run_query TodoistQuerynaut::Client.new(Todoist::Client.new("some_token"))
24
+
25
+ expect(result.size).to eq(1)
26
+ expect(result[0]["content"]).to eq("query_today_item_content")
27
+ end
28
+
29
+ it "should run a 'overdue' query" do
30
+ literal_query = TodoistQuerynaut::TodoistQuery::LiteralQuery.new("overdue", 0...7)
31
+
32
+ stub_request(:post, "https://todoist.com/API/v6/query").
33
+ with(:body => {"queries" => "[\"overdue\"]", "token" => "some_token"}).
34
+ to_return(:status => 200, :body => json_response_raw("query_overdue"), :headers => {})
35
+ result = literal_query.run_query TodoistQuerynaut::Client.new(Todoist::Client.new("some_token"))
36
+
37
+ expect(result.size).to eq(2)
38
+ expect(result[0]["content"]).to eq("overdue_one")
39
+ expect(result[1]["content"]).to eq("overdue_two")
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,22 @@
1
+ require "spec_helper"
2
+
3
+ describe TodoistQuerynaut::TodoistQuery::NegatedQuery do
4
+ describe "#run_query" do
5
+ it "should negate a 'today' query" do
6
+ negated_query = TodoistQuerynaut::Parser.parse("!today")
7
+
8
+ # will run two queries: today and "view all"
9
+ stub_request(:post, "https://todoist.com/API/v6/query").
10
+ with(:body => {"queries" => "[\"today\"]", "token" => "some_token"}).
11
+ to_return(:status => 200, :body => json_response_raw("query_today"), :headers => {})
12
+ stub_request(:post, "https://todoist.com/API/v6/query").
13
+ with(:body => {"queries" => "[\"view all\"]", "token" => "some_token"}).
14
+ to_return(:status => 200, :body => json_response_raw("query_view_all"), :headers => {})
15
+ result = negated_query.run_query TodoistQuerynaut::Client.new(Todoist::Client.new("some_token"))
16
+
17
+ # all items (4) excluding today (1) --> 3
18
+ expect(result.size).to eq(3)
19
+ end
20
+ end
21
+ end
22
+
@@ -0,0 +1,30 @@
1
+ require "spec_helper"
2
+
3
+ describe TodoistQuerynaut::TodoistQuery::PriorityQuery do
4
+ describe "#value" do
5
+ it "should return the priority of query string 'p4'" do
6
+ priority_query = TodoistQuerynaut::TodoistQuery::PriorityQuery.new("p4", 0...2)
7
+ expect(priority_query.value).to eq(4)
8
+ end
9
+
10
+ it "should return the priority of query string 'priority 3'" do
11
+ priority_query = TodoistQuerynaut::TodoistQuery::PriorityQuery.new("priority 3", 0...10)
12
+ expect(priority_query.value).to eq(3)
13
+ end
14
+ end
15
+
16
+ describe "#run_query" do
17
+ it "should run a 'p4' query" do
18
+ priority_query = TodoistQuerynaut::TodoistQuery::PriorityQuery.new("p4", 0...2)
19
+
20
+ stub_request(:post, "https://todoist.com/API/v6/query").
21
+ with(:body => {"queries" => "[\"p4\"]", "token" => "some_token"}).
22
+ to_return(:status => 200, :body => json_response_raw("query_p4"), :headers => {})
23
+ result = priority_query.run_query TodoistQuerynaut::Client.new(Todoist::Client.new("some_token"))
24
+
25
+ expect(result.size).to eq(1)
26
+ expect(result[0]["content"]).to eq("weekly_p4_item")
27
+ end
28
+ end
29
+ end
30
+
@@ -0,0 +1,44 @@
1
+ require "spec_helper"
2
+
3
+ describe TodoistQuerynaut::TodoistQuery::ProjectNameQuery do
4
+ describe "#value" do
5
+ it "should return the project name (foo) of query string 'p:foo'" do
6
+ project_name_query = TodoistQuerynaut::TodoistQuery::ProjectNameQuery.new("p:foo", 0...5)
7
+ expect(project_name_query.value).to eq("foo")
8
+ end
9
+
10
+ it "should return the project name (blarg) of query string 'p:blarg'" do
11
+ project_name_query = TodoistQuerynaut::TodoistQuery::ProjectNameQuery.new("p:blarg", 0...7)
12
+ expect(project_name_query.value).to eq("blarg")
13
+ end
14
+ end
15
+
16
+ describe "#run_query" do
17
+ before :each do
18
+ stub_request(:post, "https://todoist.com/API/v6/query").
19
+ with(:body => {"queries" => "[\"view all\"]", "token" => "some_token"}).
20
+ to_return(:status => 200, :body => json_response_raw("query_view_all"), :headers => {})
21
+ stub_request(:post, "https://todoist.com/API/v6/sync").
22
+ with(:body => { "seq_no" => "0", "seq_no_global" => "0", "resource_types" => '["projects"]', "token" => "some_token" }).
23
+ to_return(:status => 200, :body => json_response_raw("sync_projects_all"), :headers => {})
24
+ end
25
+
26
+ it "should run a 'p:Inbox' query" do
27
+ project_name_query = TodoistQuerynaut::TodoistQuery::ProjectNameQuery.new("p:Inbox", 0...7)
28
+ result = project_name_query.run_query TodoistQuerynaut::Client.new(Todoist::Client.new("some_token"))
29
+
30
+ expect(result.size).to eq(1)
31
+ expect(result[0]["content"]).to eq("query_today_item_content")
32
+ end
33
+
34
+ it "should match project names without case-sensitivity" do
35
+ project_name_query = TodoistQuerynaut::TodoistQuery::ProjectNameQuery.new("p:INBoX", 0...7)
36
+ result = project_name_query.run_query TodoistQuerynaut::Client.new(Todoist::Client.new("some_token"))
37
+
38
+ expect(result.size).to eq(1)
39
+ expect(result[0]["content"]).to eq("query_today_item_content")
40
+ end
41
+ end
42
+ end
43
+
44
+
@@ -0,0 +1,37 @@
1
+ require "spec_helper"
2
+
3
+ describe TodoistQuerynaut::TodoistQuery::Union do
4
+ describe "#run_query" do
5
+ it "should run both queries and return the union" do
6
+ union_query = TodoistQuerynaut::TodoistQuery::Union.new("today|overdue", 0...13, [
7
+ TodoistQuerynaut::TodoistQuery::LiteralQuery.new("today", 0...5),
8
+ TodoistQuerynaut::TodoistQuery::LiteralQuery.new("overdue", 0...7)
9
+ ])
10
+
11
+ stub_request(:post, "https://todoist.com/API/v6/query").
12
+ with(:body => {"queries" => "[\"today\"]", "token" => "some_token"}).
13
+ to_return(:status => 200, :body => json_response_raw("query_today"), :headers => {})
14
+ stub_request(:post, "https://todoist.com/API/v6/query").
15
+ with(:body => {"queries" => "[\"overdue\"]", "token" => "some_token"}).
16
+ to_return(:status => 200, :body => json_response_raw("query_overdue"), :headers => {})
17
+ result = union_query.run_query TodoistQuerynaut::Client.new(Todoist::Client.new("some_token"))
18
+
19
+ expect(result.size).to eq(3)
20
+ end
21
+
22
+ it "should remove duplicate items" do
23
+ union_query = TodoistQuerynaut::TodoistQuery::Union.new("today|overdue", 0...13, [
24
+ TodoistQuerynaut::TodoistQuery::LiteralQuery.new("today", 0...5),
25
+ TodoistQuerynaut::TodoistQuery::LiteralQuery.new("today", 0...5),
26
+ ])
27
+
28
+ stub_request(:post, "https://todoist.com/API/v6/query").
29
+ with(:body => {"queries" => "[\"today\"]", "token" => "some_token"}).
30
+ to_return(:status => 200, :body => json_response_raw("query_today"), :headers => {})
31
+ result = union_query.run_query TodoistQuerynaut::Client.new(Todoist::Client.new("some_token"))
32
+
33
+ expect(result.size).to eq(1)
34
+ end
35
+ end
36
+ end
37
+
@@ -0,0 +1,8 @@
1
+ require "todoist_querynaut"
2
+
3
+ require "webmock/rspec"
4
+ WebMock.disable_net_connect!
5
+
6
+ def json_response_raw(fixture_name)
7
+ File.read(File.join(__dir__, "lib/todoist_querynaut/fixtures/#{fixture_name}.json"))
8
+ end
metadata ADDED
@@ -0,0 +1,169 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: todoist_querynaut
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Stefan Siegl
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-03-03 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.7'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.7'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.4'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.4'
55
+ - !ruby/object:Gem::Dependency
56
+ name: simplecov
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: 0.11.2
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: 0.11.2
69
+ - !ruby/object:Gem::Dependency
70
+ name: webmock
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '1.17'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '1.17'
83
+ - !ruby/object:Gem::Dependency
84
+ name: ruby-todoist-api
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '0.3'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '0.3'
97
+ - !ruby/object:Gem::Dependency
98
+ name: treetop
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '1.6'
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '1.6'
111
+ description: Todoist implements its filter and query language client side, this Gem
112
+ reimplements the language features against Todoist's REST API.
113
+ email:
114
+ - stesie@brokenpipe.de
115
+ executables: []
116
+ extensions: []
117
+ extra_rdoc_files: []
118
+ files:
119
+ - LICENSE.txt
120
+ - README.md
121
+ - lib/todoist_querynaut.rb
122
+ - lib/todoist_querynaut/client.rb
123
+ - lib/todoist_querynaut/parser.rb
124
+ - lib/todoist_querynaut/project_not_found_error.rb
125
+ - lib/todoist_querynaut/todoist_query/node_extensions.rb
126
+ - lib/todoist_querynaut/todoist_query_parser.treetop
127
+ - spec/lib/todoist_querynaut/client_spec.rb
128
+ - spec/lib/todoist_querynaut/parser_spec.rb
129
+ - spec/lib/todoist_querynaut/todoist_query/intersection_spec.rb
130
+ - spec/lib/todoist_querynaut/todoist_query/literal_query_spec.rb
131
+ - spec/lib/todoist_querynaut/todoist_query/negated_query_spec.rb
132
+ - spec/lib/todoist_querynaut/todoist_query/priority_query_spec.rb
133
+ - spec/lib/todoist_querynaut/todoist_query/project_name_query_spec.rb
134
+ - spec/lib/todoist_querynaut/todoist_query/union_spec.rb
135
+ - spec/spec_helper.rb
136
+ homepage: https://github.com/stesie/todoist_querynaut
137
+ licenses:
138
+ - MIT
139
+ metadata: {}
140
+ post_install_message:
141
+ rdoc_options: []
142
+ require_paths:
143
+ - lib
144
+ required_ruby_version: !ruby/object:Gem::Requirement
145
+ requirements:
146
+ - - ">="
147
+ - !ruby/object:Gem::Version
148
+ version: '0'
149
+ required_rubygems_version: !ruby/object:Gem::Requirement
150
+ requirements:
151
+ - - ">="
152
+ - !ruby/object:Gem::Version
153
+ version: '0'
154
+ requirements: []
155
+ rubyforge_project:
156
+ rubygems_version: 2.5.1
157
+ signing_key:
158
+ specification_version: 4
159
+ summary: Todoist Query Language implementation
160
+ test_files:
161
+ - spec/lib/todoist_querynaut/todoist_query/literal_query_spec.rb
162
+ - spec/lib/todoist_querynaut/todoist_query/priority_query_spec.rb
163
+ - spec/lib/todoist_querynaut/todoist_query/union_spec.rb
164
+ - spec/lib/todoist_querynaut/todoist_query/project_name_query_spec.rb
165
+ - spec/lib/todoist_querynaut/todoist_query/negated_query_spec.rb
166
+ - spec/lib/todoist_querynaut/todoist_query/intersection_spec.rb
167
+ - spec/lib/todoist_querynaut/parser_spec.rb
168
+ - spec/lib/todoist_querynaut/client_spec.rb
169
+ - spec/spec_helper.rb