bell 0.0.1

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.
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