todoist_querynaut 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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