bell 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (67) hide show
  1. data/.gitignore +5 -0
  2. data/.rvmrc +1 -0
  3. data/Gemfile +2 -0
  4. data/Gemfile.lock +47 -0
  5. data/MIT-LICENSE +20 -0
  6. data/README.org +128 -0
  7. data/Rakefile +16 -0
  8. data/bell.gemspec +30 -0
  9. data/bin/bell +5 -0
  10. data/features/create_user.feature +16 -0
  11. data/features/full_report.feature +84 -0
  12. data/features/import_public_contacts.feature +20 -0
  13. data/features/import_user_contacts.feature +43 -0
  14. data/features/list_all_contacts.feature +19 -0
  15. data/features/list_user_contacts.feature +27 -0
  16. data/features/list_users.feature +22 -0
  17. data/features/remove_user.feature +21 -0
  18. data/features/rename_user.feature +25 -0
  19. data/features/step_definitions/cli_steps.rb +59 -0
  20. data/features/step_definitions/contact_steps.rb +104 -0
  21. data/features/step_definitions/database_steps.rb +3 -0
  22. data/features/step_definitions/report_steps.rb +17 -0
  23. data/features/step_definitions/user_steps.rb +71 -0
  24. data/features/support/env.rb +65 -0
  25. data/features/user_report.feature +67 -0
  26. data/lib/bell/cli.rb +12 -0
  27. data/lib/bell/commands/command.rb +58 -0
  28. data/lib/bell/commands/contact_command.rb +91 -0
  29. data/lib/bell/commands/implosion_command.rb +14 -0
  30. data/lib/bell/commands/report_command.rb +69 -0
  31. data/lib/bell/commands/user_command.rb +74 -0
  32. data/lib/bell/commands.rb +5 -0
  33. data/lib/bell/csv_parser.rb +52 -0
  34. data/lib/bell/dispatcher.rb +13 -0
  35. data/lib/bell/displayable.rb +13 -0
  36. data/lib/bell/full_report.rb +145 -0
  37. data/lib/bell/handlers/contacts_handler.rb +79 -0
  38. data/lib/bell/handlers/implosions_handler.rb +9 -0
  39. data/lib/bell/handlers/reports_handler.rb +41 -0
  40. data/lib/bell/handlers/users_handler.rb +50 -0
  41. data/lib/bell/handlers.rb +4 -0
  42. data/lib/bell/message.rb +115 -0
  43. data/lib/bell/public_contact.rb +17 -0
  44. data/lib/bell/user.rb +16 -0
  45. data/lib/bell/user_contact.rb +18 -0
  46. data/lib/bell/user_report.rb +88 -0
  47. data/lib/bell/util.rb +31 -0
  48. data/lib/bell.rb +157 -0
  49. data/spec/bell/cli_spec.rb +4 -0
  50. data/spec/bell/commands/command_spec.rb +59 -0
  51. data/spec/bell/commands/contact_command_spec.rb +112 -0
  52. data/spec/bell/commands/implosion_command_spec.rb +31 -0
  53. data/spec/bell/commands/report_command_spec.rb +71 -0
  54. data/spec/bell/commands/user_command_spec.rb +128 -0
  55. data/spec/bell/csv_parser_spec.rb +167 -0
  56. data/spec/bell/dispatcher_spec.rb +4 -0
  57. data/spec/bell/full_report_spec.rb +4 -0
  58. data/spec/bell/handlers/contacts_handler_spec.rb +160 -0
  59. data/spec/bell/handlers/implosions_handler_spec.rb +11 -0
  60. data/spec/bell/handlers/reports_handler_spec.rb +183 -0
  61. data/spec/bell/handlers/users_handler_spec.rb +94 -0
  62. data/spec/bell/public_contact_spec.rb +4 -0
  63. data/spec/bell/user_contact_spec.rb +4 -0
  64. data/spec/bell/user_report_spec.rb +4 -0
  65. data/spec/bell/user_spec.rb +4 -0
  66. data/spec/spec_helper.rb +21 -0
  67. metadata +230 -0
data/lib/bell/util.rb ADDED
@@ -0,0 +1,31 @@
1
+ module Bell
2
+ module Util
3
+ module String
4
+ def self.included(base)
5
+ base.send(:extend, Util::String)
6
+ end
7
+
8
+ def camelize(lower_case_and_underscored_word, first_letter_in_uppercase = true)
9
+ if first_letter_in_uppercase
10
+ lower_case_and_underscored_word.
11
+ to_s.
12
+ gsub(/\/(.?)/) { "::#{$1.upcase}" }.
13
+ gsub(/(?:^|_)(.)/) { $1.upcase }
14
+ else
15
+ lower_case_and_underscored_word.
16
+ to_s[0].
17
+ chr.
18
+ downcase + camelize(lower_case_and_underscored_word)[1..-1]
19
+ end
20
+ end
21
+
22
+ def sanitize(string)
23
+ string.unpack("C*").pack("U*")
24
+ end
25
+
26
+ def multibyte_length(string)
27
+ RUBY_VERSION < '1.9' ? string.jsize : string.size
28
+ end
29
+ end
30
+ end
31
+ end
data/lib/bell.rb ADDED
@@ -0,0 +1,157 @@
1
+ %w[embratel sequel sqlite3].each do |gem|
2
+ begin
3
+ require gem
4
+ rescue LoadError
5
+ $stderr.puts $!
6
+ exit 1
7
+ end
8
+ end
9
+
10
+ require 'fileutils'
11
+
12
+ module Bell
13
+ extend self
14
+
15
+ VERSION = '0.0.1'
16
+
17
+ class InvalidContacts < StandardError; end
18
+
19
+ def testing?
20
+ defined?(Spec) || defined?(Cucumber)
21
+ end
22
+
23
+ def environment
24
+ testing? ? 'test' : 'runtime'
25
+ end
26
+
27
+ def database
28
+ @database ||= Database.new(:environment => environment)
29
+ end
30
+
31
+ def database_connection
32
+ @database_connection ||= database.connect
33
+ end
34
+
35
+ def bootstrapped?
36
+ Directory.created? && database.created?
37
+ end
38
+
39
+ def bootstrap
40
+ Directory.create unless Directory.created?
41
+ database.create_tables?
42
+ end
43
+
44
+ def implode!
45
+ [User, UserContact, PublicContact].each(&:delete)
46
+ FileUtils.rm_rf(Directory.path)
47
+ end
48
+
49
+ def output
50
+ @output ||= Output.new
51
+ end
52
+
53
+ class Output
54
+ def initialize
55
+ @outputter = Bell.testing? ? StringIO.new : $stdout
56
+ end
57
+
58
+ def puts(text)
59
+ @outputter.puts(text)
60
+ end
61
+
62
+ def string
63
+ @outputter.string
64
+ end
65
+
66
+ def reopen
67
+ @outputter.reopen if Bell.testing?
68
+ end
69
+ end
70
+
71
+ module Directory
72
+ extend self
73
+
74
+ def path
75
+ File.join(ENV['HOME'], '.bell')
76
+ end
77
+
78
+ def created?
79
+ File.exists?(path)
80
+ end
81
+
82
+ def create
83
+ FileUtils.mkdir(path)
84
+ end
85
+ end
86
+
87
+ class Database
88
+ def initialize(options = {})
89
+ @environment = options[:environment]
90
+ end
91
+
92
+ def path
93
+ File.join(Directory.path, "bell_#{@environment}.db")
94
+ end
95
+
96
+ def created?
97
+ File.exists?(path)
98
+ end
99
+
100
+ def create_tables?
101
+ Sequel.sqlite(path) do |database|
102
+ database.create_table?(:users) do
103
+ primary_key :id
104
+ String :name, :unique => true, :null => false
105
+ end
106
+ database.create_table?(:user_contacts) do
107
+ primary_key :id
108
+ foreign_key :user_id, :null => false
109
+ String :name, :null => false
110
+ String :number, :unique => true, :null => false
111
+ end
112
+ database.create_table?(:public_contacts) do
113
+ primary_key :id
114
+ String :name, :null => false
115
+ String :number, :unique => true, :null => false
116
+ end
117
+ end
118
+ end
119
+
120
+ def connect
121
+ Sequel.sqlite(path)
122
+ end
123
+ end
124
+ end
125
+
126
+ DB = Bell.database_connection
127
+ Bell.bootstrap unless Bell.bootstrapped?
128
+
129
+ if RUBY_VERSION < '1.9'
130
+ begin
131
+ require 'fastercsv'
132
+ rescue LoadError
133
+ $stderr.puts $!
134
+ exit 1
135
+ else
136
+ Bell::CSV = FasterCSV
137
+ require 'jcode'
138
+ $KCODE = "UTF8"
139
+ end
140
+ else
141
+ require 'csv'
142
+ Bell::CSV = CSV
143
+ end
144
+
145
+ %w[util
146
+ message
147
+ displayable
148
+ commands
149
+ dispatcher
150
+ cli
151
+ csv_parser
152
+ handlers
153
+ full_report
154
+ user_report
155
+ user_contact
156
+ public_contact
157
+ user].each { |f| require File.expand_path("../bell/#{f}", __FILE__) }
@@ -0,0 +1,4 @@
1
+ require File.expand_path(File.dirname(__FILE__) << '/../spec_helper')
2
+
3
+ describe Bell::CLI do
4
+ end
@@ -0,0 +1,59 @@
1
+ require File.expand_path(File.dirname(__FILE__) << '/../../spec_helper')
2
+
3
+ describe Bell::Commands::Command do
4
+ context "when parsing" do
5
+ context "a user command" do
6
+ let(:args) { %w[user] }
7
+ let(:user_command) { mock(Bell::Commands::UserCommand) }
8
+
9
+ it "instantiates a user command and parses it" do
10
+ Bell::Commands::UserCommand.should_receive(:new).and_return(user_command)
11
+ user_command.should_receive(:parse)
12
+ described_class.new(args).parse
13
+ end
14
+ end
15
+
16
+ context "a contact command" do
17
+ let(:args) { %w[contact] }
18
+ let(:contact_command) { mock(Bell::Commands::ContactCommand) }
19
+
20
+ it "instantiates a contact command and parses it" do
21
+ Bell::Commands::ContactCommand.should_receive(:new).and_return(contact_command)
22
+ contact_command.should_receive(:parse)
23
+ described_class.new(args).parse
24
+ end
25
+ end
26
+
27
+ context "a report command" do
28
+ let(:args) { %w[report] }
29
+ let(:report_command) { mock(Bell::Commands::ReportCommand) }
30
+
31
+ it "instantiates a report command and parses it" do
32
+ Bell::Commands::ReportCommand.should_receive(:new).and_return(report_command)
33
+ report_command.should_receive(:parse)
34
+ described_class.new(args).parse
35
+ end
36
+ end
37
+
38
+ context "an implosion command" do
39
+ let(:args) { %w[implode] }
40
+ let(:implosion_command) { mock(Bell::Commands::ImplosionCommand) }
41
+
42
+ it "instantiates an implosion command and parses it" do
43
+ Bell::Commands::ImplosionCommand.should_receive(:new).and_return(implosion_command)
44
+ implosion_command.should_receive(:parse)
45
+ described_class.new(args).parse
46
+ end
47
+ end
48
+
49
+ context "an unknown command" do
50
+ let(:args) { %w[foo] }
51
+ let(:contact_command) { mock(Bell::Commands::ContactCommand) }
52
+
53
+ it "instantiates an implosion command and parses it" do
54
+ lambda { described_class.new(args).parse }.
55
+ should raise_error(ArgumentError, described_class::USAGE)
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,112 @@
1
+ require File.expand_path(File.dirname(__FILE__) << '/../../spec_helper')
2
+
3
+ describe Bell::Commands::ContactCommand do
4
+ describe ".initialize" do
5
+ def contact_command_route(action)
6
+ { :handler => 'contacts_handler', :action => action, :params => {} }
7
+ end
8
+
9
+ context "when given invalid actions" do
10
+ it "raises ArgumentError" do
11
+ lambda { described_class.new(%w[foo]).parse }.
12
+ should raise_error(ArgumentError, described_class::USAGE)
13
+ end
14
+ end
15
+
16
+ context "when giver valid actions" do
17
+ it "sets the handler route key" do
18
+ %w[import list].each do |action|
19
+ contact_command = described_class.new([action])
20
+ contact_command.route.should == contact_command_route(action)
21
+ end
22
+ end
23
+ end
24
+ end
25
+
26
+ describe ".import" do
27
+ context "when given no additional arguments" do
28
+ it "raises ArgumentError" do
29
+ lambda { described_class.new(%w[import]).parse }.
30
+ should raise_error(ArgumentError, described_class::IMPORT_USAGE)
31
+ end
32
+ end
33
+
34
+ context "when given valid arguments" do
35
+ let(:path) { 'path/to/contacts_file.csv' }
36
+ let(:user_name) { 'john' }
37
+ let(:contact_command_route) do
38
+ { :handler => 'contacts_handler',
39
+ :action => 'import',
40
+ :params => { :path => path, :user => { :name => user_name } } }
41
+ end
42
+
43
+ it "assembles the right route" do
44
+ %w[-u --user].each do |user_name_flag|
45
+ described_class.
46
+ new(['import', path, user_name_flag, user_name]).
47
+ parse.
48
+ route.should == contact_command_route
49
+ end
50
+ end
51
+ end
52
+ end
53
+
54
+ context "parsing 'list' commands" do
55
+ context "without additional arguments" do
56
+ let(:contact_command_route) do
57
+ { :handler => 'contacts_handler',
58
+ :action => 'list',
59
+ :params => {} }
60
+ end
61
+
62
+ it "assembles the right route" do
63
+ described_class.new(%w[list]).parse.route.should == contact_command_route
64
+ end
65
+ end
66
+
67
+ context "when given valid user arguments" do
68
+ context "when not asking for CSV" do
69
+ let(:user_name) { 'john' }
70
+ let(:contact_command_route) do
71
+ { :handler => 'contacts_handler',
72
+ :action => 'list',
73
+ :params => { :user => { :name => user_name } } }
74
+ end
75
+
76
+ it "assembles the right route" do
77
+ %w[-u --user].each do |user_name_flag|
78
+ described_class.
79
+ new(['list', user_name_flag, user_name]).
80
+ parse.
81
+ route.should == contact_command_route
82
+ end
83
+ end
84
+ end
85
+
86
+ context "when asking for CSV" do
87
+ let(:user_name) { 'john' }
88
+ let(:contact_command_route) do
89
+ { :handler => 'contacts_handler',
90
+ :action => 'list',
91
+ :params => { :user => { :name => user_name }, :csv => true } }
92
+ end
93
+
94
+ it "assembles the right route" do
95
+ %w[-u --user].each do |user_name_flag|
96
+ described_class.
97
+ new(['list', user_name_flag, user_name, '--csv']).
98
+ parse.
99
+ route.should == contact_command_route
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
105
+
106
+ context "parsing unknown commands" do
107
+ it "raises ArgumentError" do
108
+ lambda { described_class.new(%w[foo]).parse }.
109
+ should raise_error(ArgumentError, described_class::USAGE)
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,31 @@
1
+ require File.expand_path(File.dirname(__FILE__) << '/../../spec_helper')
2
+
3
+ describe Bell::Commands::ImplosionCommand do
4
+ context "when initialized" do
5
+ let(:implosion_command_route) do
6
+ { :handler => 'implosions_handler', :action => nil, :params => {} }
7
+ end
8
+
9
+ it "has default values" do
10
+ implosion_command = described_class.new
11
+ implosion_command.route.should == implosion_command_route
12
+ end
13
+ end
14
+
15
+ context "parsing" do
16
+ context "without additional arguments" do
17
+ let(:args) { %w[path/to/fatura.csv] }
18
+ let(:implosion_command_route) do
19
+ { :handler => 'implosions_handler',
20
+ :action => 'implode',
21
+ :params => {} }
22
+ end
23
+
24
+ it "assembles the right route" do
25
+ implosion_command = described_class.new(args)
26
+ implosion_command.parse
27
+ implosion_command.route.should == implosion_command_route
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,71 @@
1
+ require File.expand_path(File.dirname(__FILE__) << '/../../spec_helper')
2
+
3
+ describe Bell::Commands::ReportCommand do
4
+ context "when initialized" do
5
+ let(:report_command_route) do
6
+ { :handler => 'reports_handler', :action => nil, :params => {} }
7
+ end
8
+
9
+ it "has default values" do
10
+ report_command = described_class.new
11
+ report_command.route.should == report_command_route
12
+ end
13
+ end
14
+
15
+ context "parsing 'full_report' commands" do
16
+ context "with additional arguments" do
17
+ let(:args) { %w[fatura.csv] }
18
+ let(:report_command_route) do
19
+ { :handler => 'reports_handler',
20
+ :action => 'full_report',
21
+ :params => { :path => args[0] } }
22
+ end
23
+
24
+ it "assembles the right route" do
25
+ report_command = described_class.new(args)
26
+ report_command.parse
27
+ report_command.route.should == report_command_route
28
+ end
29
+ end
30
+
31
+ context "without additional arguments" do
32
+ let(:args) { %w[] }
33
+
34
+ it "raises ArgumentError" do
35
+ lambda { described_class.new(args).parse }.
36
+ should raise_error(ArgumentError, described_class::FULL_REPORT_USAGE)
37
+ end
38
+ end
39
+ end
40
+
41
+ context "parsing 'user_report' commands" do
42
+ context "when given all necessary arguments" do
43
+ let(:args) { %w[fatura.csv -u bob] }
44
+ let(:report_command_route) do
45
+ { :handler => 'reports_handler',
46
+ :action => 'user_report',
47
+ :params => { :path => args[0], :user => { :name => args[2] } } }
48
+ end
49
+
50
+ it "assembles the right route" do
51
+ report_command = described_class.new(args)
52
+ report_command.parse
53
+ report_command.route.should == report_command_route
54
+ end
55
+ end
56
+
57
+ context "with one missing argument" do
58
+ let(:args) { %w[-u bob] }
59
+ let(:args_with_one_missing_array) do
60
+ args.dup.inject([]) { |args_with_one_missing, arg|
61
+ args_with_one_missing << args.reject { |a| a == arg }
62
+ }.each { |array| array.unshift('fatura.csv') }
63
+ end
64
+
65
+ it "raises ArgumentError" do
66
+ lambda { described_class.new(args).parse }.
67
+ should raise_error(ArgumentError, described_class::USER_REPORT_USAGE)
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,128 @@
1
+ require File.expand_path(File.dirname(__FILE__) << '/../../spec_helper')
2
+
3
+ describe Bell::Commands::UserCommand do
4
+ context "when initialized" do
5
+ let(:user_command_route) do
6
+ { :handler => 'users_handler', :action => nil, :params => {} }
7
+ end
8
+
9
+ it "has default values" do
10
+ user_command = described_class.new
11
+ user_command.route.should == user_command_route
12
+ end
13
+ end
14
+
15
+ context "parsing 'create' commands" do
16
+ context "without additional arguments" do
17
+ let(:args) { %w[create] }
18
+
19
+ it "raises ArgumentError" do
20
+ lambda { described_class.new(args).parse }.
21
+ should raise_error(ArgumentError, described_class::CREATE_USAGE)
22
+ end
23
+ end
24
+
25
+ context "with one additional argument" do
26
+ let(:args) { %w[create bob] }
27
+ let(:user_command_route) do
28
+ { :handler => 'users_handler',
29
+ :action => 'create',
30
+ :params => { :user => { :name => args.last } } }
31
+ end
32
+
33
+ it "assembles the right route" do
34
+ user_command = described_class.new(args)
35
+ user_command.parse
36
+ user_command.route.should == user_command_route
37
+ end
38
+ end
39
+ end
40
+
41
+ context "parsing 'rename' commands" do
42
+ context "without additional arguments" do
43
+ let(:args) { %w[rename] }
44
+
45
+ it "raises ArgumentError" do
46
+ lambda { described_class.new(args).parse }.
47
+ should raise_error(ArgumentError, described_class::RENAME_USAGE)
48
+ end
49
+ end
50
+
51
+ context "with one additional argument" do
52
+ let(:args) { %w[rename bob] }
53
+
54
+ it "raises ArgumentError" do
55
+ lambda { described_class.new(args).parse }.
56
+ should raise_error(ArgumentError, described_class::RENAME_USAGE)
57
+ end
58
+ end
59
+
60
+ context "with two additional arguments" do
61
+ let(:args) { %w[rename bob robert] }
62
+ let(:user_command_route) do
63
+ { :handler => 'users_handler',
64
+ :action => 'rename',
65
+ :params => { :user => { :source_name => args[1],
66
+ :target_name => args[2] } } }
67
+ end
68
+
69
+ it "assembles the right route" do
70
+ user_command = described_class.new(args)
71
+ user_command.parse
72
+ user_command.route.should == user_command_route
73
+ end
74
+ end
75
+ end
76
+
77
+ context "parsing 'list' commands" do
78
+ context "without additional arguments" do
79
+ let(:args) { %w[list] }
80
+ let(:user_command_route) do
81
+ { :handler => 'users_handler',
82
+ :action => 'list',
83
+ :params => {} }
84
+ end
85
+
86
+ it "assembles the right route" do
87
+ user_command = described_class.new(args)
88
+ user_command.parse
89
+ user_command.route.should == user_command_route
90
+ end
91
+ end
92
+ end
93
+
94
+ context "parsing 'remove' commands" do
95
+ context "without additional arguments" do
96
+ let(:args) { %w[remove] }
97
+
98
+ it "raises ArgumentError" do
99
+ lambda { described_class.new(args).parse }.
100
+ should raise_error(ArgumentError, described_class::REMOVE_USAGE)
101
+ end
102
+ end
103
+
104
+ context "with one additional argument" do
105
+ let(:args) { %w[remove bob] }
106
+ let(:user_command_route) do
107
+ { :handler => 'users_handler',
108
+ :action => 'remove',
109
+ :params => { :user => { :name => args.last } } }
110
+ end
111
+
112
+ it "assembles the right route" do
113
+ user_command = described_class.new(args)
114
+ user_command.parse
115
+ user_command.route.should == user_command_route
116
+ end
117
+ end
118
+ end
119
+
120
+ context "parsing unknown commands" do
121
+ let(:args) { %w[foo] }
122
+
123
+ it "raises ArgumentError" do
124
+ lambda { described_class.new(args).parse }.
125
+ should raise_error(ArgumentError, described_class::USAGE)
126
+ end
127
+ end
128
+ end