gyunyu 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,16 @@
1
+ # -*- coding: utf-8 -*-
2
+ require 'json'
3
+
4
+ module Gyunyu
5
+ module Command
6
+ module Export
7
+ module Format
8
+ module Json
9
+ def self.export( tasks, fields )
10
+ tasks.to_json
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,16 @@
1
+ # -*- coding: utf-8 -*-
2
+ require 'yaml'
3
+
4
+ module Gyunyu
5
+ module Command
6
+ module Export
7
+ module Format
8
+ module Yaml
9
+ def self.export( tasks, fields )
10
+ tasks.to_yaml
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -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
@@ -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