gyunyu 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.rspec +1 -0
- data/Gemfile +26 -0
- data/LICENSE.txt +20 -0
- data/README.rdoc +35 -0
- data/Rakefile +46 -0
- data/VERSION +1 -0
- data/bin/gyunyu +11 -0
- data/gyunyu.gemspec +92 -0
- data/lib/gyunyu.rb +9 -0
- data/lib/gyunyu/app.rb +4 -0
- data/lib/gyunyu/command.rb +21 -0
- data/lib/gyunyu/command/export/app.rb +150 -0
- data/lib/gyunyu/command/export/format/csv.rb +27 -0
- data/lib/gyunyu/command/export/format/json.rb +16 -0
- data/lib/gyunyu/command/export/format/yaml.rb +16 -0
- data/lib/gyunyu/command/export/option.rb +63 -0
- data/lib/gyunyu/expander.rb +40 -0
- data/lib/gyunyu/token.rb +89 -0
- data/spec/app/.gitkeep +0 -0
- data/spec/app/command/export/option_spec.rb +137 -0
- data/spec/app/command_spec.rb +62 -0
- data/spec/app/expander_spec.rb +140 -0
- data/spec/spec_helper.rb +12 -0
- data/spec/token/token_spec.rb +87 -0
- data/tasks/git.rake +7 -0
- data/tasks/spec.rake +11 -0
- data/tmp/.gitkeep +0 -0
- metadata +178 -0
@@ -0,0 +1,63 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
require 'optparse'
|
3
|
+
|
4
|
+
module Gyunyu
|
5
|
+
module Command
|
6
|
+
module Export
|
7
|
+
class Option
|
8
|
+
class FormatNotFound < StandardError; end
|
9
|
+
|
10
|
+
FIELD_SEP = ','
|
11
|
+
|
12
|
+
def initialize( argv = [] )
|
13
|
+
@lists = []
|
14
|
+
@filter = nil
|
15
|
+
@fields = []
|
16
|
+
@format = :csv
|
17
|
+
|
18
|
+
parser.parse( argv )
|
19
|
+
end
|
20
|
+
attr_reader :lists, :filter, :fields, :format
|
21
|
+
|
22
|
+
def fields
|
23
|
+
if @fields.size > 0
|
24
|
+
@fields
|
25
|
+
else
|
26
|
+
%w( task_id name due )
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def parser
|
31
|
+
OptionParser.new do |opt|
|
32
|
+
opt.on('-l', '--list LIST') { |list|
|
33
|
+
if !@lists.include?( list )
|
34
|
+
@lists << list
|
35
|
+
end
|
36
|
+
}
|
37
|
+
opt.on('-f', '--filter FILTER') { |filter|
|
38
|
+
@filter = filter if filter.size > 0
|
39
|
+
}
|
40
|
+
opt.on('-d', '--field FIELD') { |field|
|
41
|
+
if field.include?( FIELD_SEP )
|
42
|
+
@fields = field.split( FIELD_SEP )
|
43
|
+
elsif !@fields.include?( field )
|
44
|
+
@fields << field
|
45
|
+
end
|
46
|
+
}
|
47
|
+
opt.on('-o', '--format FORMAT') { |format|
|
48
|
+
format.downcase!
|
49
|
+
formats = Format.constants.map { |e|
|
50
|
+
e.to_s.downcase
|
51
|
+
}
|
52
|
+
if formats.include?( format )
|
53
|
+
@format = format
|
54
|
+
else
|
55
|
+
raise FormatNotFound
|
56
|
+
end
|
57
|
+
}
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Gyunyu
|
2
|
+
module Expander
|
3
|
+
class << self
|
4
|
+
#
|
5
|
+
# [param] Array or Hash taskseries
|
6
|
+
# [return] Array
|
7
|
+
#
|
8
|
+
def taskseries( list )
|
9
|
+
expanded = []
|
10
|
+
|
11
|
+
if list.is_a?( Hash ) and list.has_key?( 'taskseries' )
|
12
|
+
list = list['taskseries']
|
13
|
+
end
|
14
|
+
|
15
|
+
list.each { |series|
|
16
|
+
expanded += tasks( series )
|
17
|
+
}
|
18
|
+
|
19
|
+
expanded
|
20
|
+
end
|
21
|
+
|
22
|
+
#
|
23
|
+
# [param] Hash taskseries
|
24
|
+
# [return] Array
|
25
|
+
#
|
26
|
+
def tasks( taskseries )
|
27
|
+
task = Marshal.load( Marshal.dump( taskseries ) )
|
28
|
+
task.delete('id')
|
29
|
+
task.delete('task')
|
30
|
+
task['taskseries_id'] = taskseries['id']
|
31
|
+
|
32
|
+
taskseries['task'].map { |t|
|
33
|
+
t['task_id'] = t['id']
|
34
|
+
t.delete( 'id' )
|
35
|
+
task.merge( t )
|
36
|
+
}
|
37
|
+
end
|
38
|
+
end # of << self
|
39
|
+
end
|
40
|
+
end
|
data/lib/gyunyu/token.rb
ADDED
@@ -0,0 +1,89 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
|
3
|
+
module Gyunyu
|
4
|
+
class Token
|
5
|
+
API_KEY = '0af285d87a0798cdd4751acfb02fc058'
|
6
|
+
SHARED_SECRET = 'f0611feb04a04e1d'
|
7
|
+
|
8
|
+
include ::FileUtils
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
RTM::API.init( API_KEY, SHARED_SECRET )
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.get
|
15
|
+
@self = self.new
|
16
|
+
token = @self.load
|
17
|
+
|
18
|
+
if ( !token )
|
19
|
+
token = @self.request
|
20
|
+
@self.save( token )
|
21
|
+
end
|
22
|
+
|
23
|
+
RTM::API.token = token
|
24
|
+
token
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.purge
|
28
|
+
RTM::API.token = nil
|
29
|
+
self.new.instance_eval {
|
30
|
+
while ( File.exist?( magazine ) )
|
31
|
+
rm( magazine )
|
32
|
+
end
|
33
|
+
}
|
34
|
+
end
|
35
|
+
|
36
|
+
def save( token )
|
37
|
+
if ( !File.exist?( magazine ) )
|
38
|
+
open( magazine, 'w', 0600 ) { }
|
39
|
+
end
|
40
|
+
|
41
|
+
open( magazine, 'r+b', 0600 ) { |f|
|
42
|
+
f.flock( File::LOCK_EX )
|
43
|
+
f.truncate(0)
|
44
|
+
f.print token
|
45
|
+
f.flock( File::LOCK_UN )
|
46
|
+
}
|
47
|
+
token
|
48
|
+
end
|
49
|
+
|
50
|
+
def load
|
51
|
+
token = nil
|
52
|
+
|
53
|
+
if ( File.exist?( magazine ) )
|
54
|
+
open( magazine, 'r+b' ) { |f|
|
55
|
+
# TODO WARN if permission is too loose
|
56
|
+
f.flock( File::LOCK_SH )
|
57
|
+
token = f.read
|
58
|
+
f.flock( File::LOCK_UN )
|
59
|
+
}
|
60
|
+
end
|
61
|
+
|
62
|
+
token
|
63
|
+
end
|
64
|
+
|
65
|
+
def magazine
|
66
|
+
env || dotfile
|
67
|
+
end
|
68
|
+
|
69
|
+
def env
|
70
|
+
ENV['RTMSTAT_TOKEN']
|
71
|
+
end
|
72
|
+
|
73
|
+
def dotfile
|
74
|
+
File.join( ENV['HOME'], '.gyunyu' )
|
75
|
+
end
|
76
|
+
|
77
|
+
def request
|
78
|
+
frob = RTM::Auth::GetFrob.new.invoke
|
79
|
+
url = RTM::API.get_auth_url('read', frob)
|
80
|
+
puts "open RTM with browser and accept API KEY."
|
81
|
+
puts "and hit enter."
|
82
|
+
system("open '#{url}'")
|
83
|
+
STDIN.gets
|
84
|
+
|
85
|
+
res = RTM::Auth::GetToken.new(frob).invoke
|
86
|
+
res[:token]
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
data/spec/app/.gitkeep
ADDED
File without changes
|
@@ -0,0 +1,137 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
require File.dirname(__FILE__) + '/../../../spec_helper'
|
3
|
+
|
4
|
+
describe Gyunyu::Command::Export::Option do
|
5
|
+
describe 'parser' do
|
6
|
+
subject {
|
7
|
+
Gyunyu::Command::Export::Option.new.parser
|
8
|
+
}
|
9
|
+
it {
|
10
|
+
subject.class.should == OptionParser
|
11
|
+
}
|
12
|
+
|
13
|
+
describe 'lists' do
|
14
|
+
context 'no lists' do
|
15
|
+
subject {
|
16
|
+
Gyunyu::Command::Export::Option.new.lists
|
17
|
+
}
|
18
|
+
it {
|
19
|
+
should == []
|
20
|
+
}
|
21
|
+
end
|
22
|
+
context 'one list' do
|
23
|
+
subject {
|
24
|
+
Gyunyu::Command::Export::Option.new( %w( -l 仕事 ) ).lists
|
25
|
+
}
|
26
|
+
it {
|
27
|
+
should == %w( 仕事 )
|
28
|
+
}
|
29
|
+
end
|
30
|
+
context 'two lists' do
|
31
|
+
subject {
|
32
|
+
Gyunyu::Command::Export::Option.new( %w( -l 仕事 -l 勉強 ) ).lists
|
33
|
+
}
|
34
|
+
it {
|
35
|
+
should == %w( 仕事 勉強 )
|
36
|
+
}
|
37
|
+
end
|
38
|
+
context 'twice same list' do
|
39
|
+
subject {
|
40
|
+
Gyunyu::Command::Export::Option.new( %w( -l 仕事 -l 仕事 ) ).lists
|
41
|
+
}
|
42
|
+
it {
|
43
|
+
should == %w( 仕事 )
|
44
|
+
}
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
describe 'filter' do
|
49
|
+
context 'no filter' do
|
50
|
+
subject {
|
51
|
+
Gyunyu::Command::Export::Option.new( %w() ).filter
|
52
|
+
}
|
53
|
+
it {
|
54
|
+
should be_nil
|
55
|
+
}
|
56
|
+
end
|
57
|
+
context 'one filter' do
|
58
|
+
subject {
|
59
|
+
Gyunyu::Command::Export::Option.new( %w( -f filter ) ).filter
|
60
|
+
}
|
61
|
+
it {
|
62
|
+
should == 'filter'
|
63
|
+
}
|
64
|
+
end
|
65
|
+
context 'multiple filters' do
|
66
|
+
subject {
|
67
|
+
Gyunyu::Command::Export::Option.new( %W( -f first\ one -f last\ one ) ).filter
|
68
|
+
}
|
69
|
+
it {
|
70
|
+
should == 'last one'
|
71
|
+
}
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
describe 'fields' do
|
76
|
+
context 'no fields' do
|
77
|
+
subject {
|
78
|
+
Gyunyu::Command::Export::Option.new().fields
|
79
|
+
}
|
80
|
+
it {
|
81
|
+
should == %w( task_id name due )
|
82
|
+
}
|
83
|
+
end
|
84
|
+
context 'specified fields' do
|
85
|
+
subject {
|
86
|
+
Gyunyu::Command::Export::Option.new( %w( -d id ) ).fields
|
87
|
+
}
|
88
|
+
it {
|
89
|
+
should == %w( id )
|
90
|
+
}
|
91
|
+
end
|
92
|
+
context 'duplicated fields' do
|
93
|
+
subject {
|
94
|
+
Gyunyu::Command::Export::Option.new( %w( -d id -d name -d id ) ).fields
|
95
|
+
}
|
96
|
+
it {
|
97
|
+
should == %w( id name )
|
98
|
+
}
|
99
|
+
end
|
100
|
+
context 'include #{FIELD_SEP}' do
|
101
|
+
subject {
|
102
|
+
Gyunyu::Command::Export::Option.new( %w( -d id,name,taskseries_id,due ) ).fields
|
103
|
+
}
|
104
|
+
it 'bulk definition' do
|
105
|
+
should == %w( id name taskseries_id due )
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
describe 'format' do
|
111
|
+
context 'no format' do
|
112
|
+
subject {
|
113
|
+
Gyunyu::Command::Export::Option.new().format
|
114
|
+
}
|
115
|
+
it {
|
116
|
+
should == :csv
|
117
|
+
}
|
118
|
+
end
|
119
|
+
context 'correct format' do
|
120
|
+
subject {
|
121
|
+
Gyunyu::Command::Export::Option.new( %w( -o json ) ).format
|
122
|
+
}
|
123
|
+
it {
|
124
|
+
should == 'json'
|
125
|
+
}
|
126
|
+
end
|
127
|
+
context 'wrong format' do
|
128
|
+
subject {
|
129
|
+
Gyunyu::Command::Export::Option.new( %w( -o atom ) ).format
|
130
|
+
}
|
131
|
+
it {
|
132
|
+
lambda { subject }.should raise_error( Gyunyu::Command::Export::Option::FormatNotFound )
|
133
|
+
}
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../spec_helper'
|
2
|
+
|
3
|
+
describe Gyunyu::Command do
|
4
|
+
describe 'commands' do
|
5
|
+
let(:commands) {
|
6
|
+
Gyunyu::Command.commands
|
7
|
+
}
|
8
|
+
describe 'array' do
|
9
|
+
it {
|
10
|
+
commands.size.should > 0
|
11
|
+
}
|
12
|
+
end
|
13
|
+
describe 'array of strings' do
|
14
|
+
it 'array of strings' do
|
15
|
+
commands.map { |e| e.should.class == String }
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
describe 'dispatch' do
|
21
|
+
context 'correct command' do
|
22
|
+
before {
|
23
|
+
stub.instance_of(Gyunyu::Command::Export::App).run {
|
24
|
+
"`export' dispatched"
|
25
|
+
}
|
26
|
+
}
|
27
|
+
subject {
|
28
|
+
Gyunyu::Command.dispatch( 'export' )
|
29
|
+
}
|
30
|
+
it {
|
31
|
+
should == "`export' dispatched"
|
32
|
+
}
|
33
|
+
end
|
34
|
+
context 'wrong command' do
|
35
|
+
subject {
|
36
|
+
Gyunyu::Command.dispatch( 'foo' )
|
37
|
+
}
|
38
|
+
it {
|
39
|
+
lambda {subject}.should raise_error(Gyunyu::Command::NotFound)
|
40
|
+
}
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
describe 'app_const' do
|
45
|
+
context 'correct app' do
|
46
|
+
subject {
|
47
|
+
Gyunyu::Command.app_const( 'export' )
|
48
|
+
}
|
49
|
+
it {
|
50
|
+
should == Gyunyu::Command::Export::App
|
51
|
+
}
|
52
|
+
end
|
53
|
+
context 'wrong app' do
|
54
|
+
subject {
|
55
|
+
Gyunyu::Command.app_const( 'foo' )
|
56
|
+
}
|
57
|
+
it {
|
58
|
+
lambda {subject}.should raise_error( NameError )
|
59
|
+
}
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,140 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
require File.dirname(__FILE__) + '/../spec_helper'
|
3
|
+
|
4
|
+
describe Gyunyu::Expander do
|
5
|
+
describe 'taskseries' do
|
6
|
+
shared_examples_for :taskseries do
|
7
|
+
it 'array of hash' do
|
8
|
+
subject.class.should == Array
|
9
|
+
subject.each { |e|
|
10
|
+
e.class.should == Hash
|
11
|
+
}
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
context 'array given' do
|
16
|
+
it_should_behave_like :taskseries do
|
17
|
+
subject {
|
18
|
+
Gyunyu::Expander.taskseries( task_raw_data.first['taskseries'] )
|
19
|
+
}
|
20
|
+
end
|
21
|
+
end
|
22
|
+
context 'hash given' do
|
23
|
+
it_should_behave_like :taskseries do
|
24
|
+
subject {
|
25
|
+
Gyunyu::Expander.taskseries( task_raw_data.first )
|
26
|
+
}
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
describe 'tasks' do
|
32
|
+
subject {
|
33
|
+
Gyunyu::Expander.tasks(task_raw_data.first['taskseries'].first)
|
34
|
+
}
|
35
|
+
it {
|
36
|
+
should == [
|
37
|
+
{ "taskseries_id"=>"118872028",
|
38
|
+
"created"=>"2011-05-28T01:04:49Z",
|
39
|
+
"modified"=>"2011-10-10T22:00:53Z",
|
40
|
+
"name"=>"jsのlintについて",
|
41
|
+
"source"=>"js",
|
42
|
+
"url"=>"",
|
43
|
+
"location_id"=>"",
|
44
|
+
"tags"=>[{"tag"=>["research", "writing"]}],
|
45
|
+
"participants"=>[{}],
|
46
|
+
"notes"=>
|
47
|
+
[{"note"=>
|
48
|
+
[{"id"=>"26111105",
|
49
|
+
"created"=>"2011-10-10T22:00:53Z",
|
50
|
+
"modified"=>"2011-10-10T22:00:53Z",
|
51
|
+
"title"=>"",
|
52
|
+
"content"=>"意図が分かんないな、このメモ。結局何をしたかった?"}]}],
|
53
|
+
"task_id"=>"181820465",
|
54
|
+
"due"=>"",
|
55
|
+
"has_due_time"=>"0",
|
56
|
+
"added"=>"2011-05-28T01:04:49Z",
|
57
|
+
"completed"=>"",
|
58
|
+
"deleted"=>"",
|
59
|
+
"priority"=>"1",
|
60
|
+
"postponed"=>"0",
|
61
|
+
"estimate"=>""}]
|
62
|
+
}
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def task_raw_data
|
67
|
+
# [{'id' => list id,
|
68
|
+
# 'taskseries' =>
|
69
|
+
# []
|
70
|
+
# }]
|
71
|
+
[{"id"=>"135243",
|
72
|
+
"taskseries"=>
|
73
|
+
[{"id"=>"118872028",
|
74
|
+
"created"=>"2011-05-28T01:04:49Z",
|
75
|
+
"modified"=>"2011-10-10T22:00:53Z",
|
76
|
+
"name"=>"jsのlintについて",
|
77
|
+
"source"=>"js",
|
78
|
+
"url"=>"",
|
79
|
+
"location_id"=>"",
|
80
|
+
"tags"=>[{"tag"=>["research", "writing"]}],
|
81
|
+
"participants"=>[{}],
|
82
|
+
"notes"=>
|
83
|
+
[{"note"=>
|
84
|
+
[{"id"=>"26111105",
|
85
|
+
"created"=>"2011-10-10T22:00:53Z",
|
86
|
+
"modified"=>"2011-10-10T22:00:53Z",
|
87
|
+
"title"=>"",
|
88
|
+
"content"=>"意図が分かんないな、このメモ。結局何をしたかった?"}]}],
|
89
|
+
"task"=>
|
90
|
+
[{"id"=>"181820465",
|
91
|
+
"due"=>"",
|
92
|
+
"has_due_time"=>"0",
|
93
|
+
"added"=>"2011-05-28T01:04:49Z",
|
94
|
+
"completed"=>"",
|
95
|
+
"deleted"=>"",
|
96
|
+
"priority"=>"1",
|
97
|
+
"postponed"=>"0",
|
98
|
+
"estimate"=>""}]},
|
99
|
+
{"id"=>"46116936",
|
100
|
+
"created"=>"2009-08-01T08:35:30Z",
|
101
|
+
"modified"=>"2010-05-10T03:18:43Z",
|
102
|
+
"name"=>"あの本読む",
|
103
|
+
"source"=>"js",
|
104
|
+
"url"=>"",
|
105
|
+
"location_id"=>"",
|
106
|
+
"tags"=>[{"tag"=>["reading"]}],
|
107
|
+
"participants"=>[{}],
|
108
|
+
"notes"=>[{}],
|
109
|
+
"task"=>
|
110
|
+
[{"id"=>"65881131",
|
111
|
+
"due"=>"",
|
112
|
+
"has_due_time"=>"0",
|
113
|
+
"added"=>"2009-08-01T08:35:30Z",
|
114
|
+
"completed"=>"",
|
115
|
+
"deleted"=>"",
|
116
|
+
"priority"=>"N",
|
117
|
+
"postponed"=>"0",
|
118
|
+
"estimate"=>"1時間"}]},
|
119
|
+
{"id"=>"115609158",
|
120
|
+
"created"=>"2011-05-02T10:13:47Z",
|
121
|
+
"modified"=>"2011-05-07T12:26:43Z",
|
122
|
+
"name"=>"この本読む",
|
123
|
+
"source"=>"js",
|
124
|
+
"url"=>"",
|
125
|
+
"location_id"=>"",
|
126
|
+
"tags"=>[{"tag"=>["reading"]}],
|
127
|
+
"participants"=>[{}],
|
128
|
+
"notes"=>[{}],
|
129
|
+
"task"=>
|
130
|
+
[{"id"=>"176273444",
|
131
|
+
"due"=>"",
|
132
|
+
"has_due_time"=>"0",
|
133
|
+
"added"=>"2011-05-02T10:13:47Z",
|
134
|
+
"completed"=>"",
|
135
|
+
"deleted"=>"",
|
136
|
+
"priority"=>"1",
|
137
|
+
"postponed"=>"0",
|
138
|
+
"estimate"=>"8h"}]}
|
139
|
+
]}]
|
140
|
+
end
|