billtrap 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,19 @@
1
+ module Serenity
2
+ module Debug
3
+ def debug?
4
+ false
5
+ end
6
+
7
+ def debug_file_path
8
+ File.join(debug_dir, debug_file_name)
9
+ end
10
+
11
+ def debug_file_name
12
+ "serenity_debug_#{rand(100)}.rb"
13
+ end
14
+
15
+ def debug_dir
16
+ File.join(File.dirname(__FILE__), '..', '..', 'debug')
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,18 @@
1
+ class String
2
+ def escape_xml
3
+ mgsub!([[/&/, '&amp;'], [/</, '&lt;'], [/>/, '&gt;']])
4
+ end
5
+
6
+ def convert_newlines
7
+ gsub!("\n", '<text:line-break/>')
8
+ self
9
+ end
10
+
11
+ def mgsub!(key_value_pairs=[].freeze)
12
+ regexp_fragments = key_value_pairs.collect { |k,v| k }
13
+ gsub!(Regexp.union(*regexp_fragments)) do |match|
14
+ key_value_pairs.detect{|k,v| k =~ match}[1]
15
+ end
16
+ self
17
+ end
18
+ end
@@ -0,0 +1,18 @@
1
+ module Serenity
2
+ module Generator
3
+ def render_odt template_path, output_path = output_name(template_path)
4
+ template = Template.new template_path, output_path
5
+ template.process binding
6
+ end
7
+
8
+ private
9
+
10
+ def output_name input
11
+ if input =~ /(.+)\.odt\Z/
12
+ "#{$1}_output.odt"
13
+ else
14
+ "#{input}_output.odt"
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,68 @@
1
+ module Serenity
2
+ class Line
3
+ attr_reader :text
4
+
5
+ def initialize text
6
+ @text = text
7
+ end
8
+
9
+ def to_s
10
+ @text
11
+ end
12
+
13
+ def self.text txt
14
+ TextLine.new txt
15
+ end
16
+
17
+ def self.code txt
18
+ CodeLine.new txt
19
+ end
20
+
21
+ def self.string txt
22
+ StringLine.new txt
23
+ end
24
+
25
+ def self.literal txt
26
+ LiteralLine.new txt
27
+ end
28
+
29
+ end
30
+
31
+ class TextLine < Line
32
+ def to_buf
33
+ " _buf << '" << escape_text(@text) << "';"
34
+ end
35
+
36
+ def escape_text text
37
+ text.gsub(/['\\]/, '\\\\\&')
38
+ end
39
+ end
40
+
41
+ class CodeLine < Line
42
+ def to_buf
43
+ escape_code(@text) << ';'
44
+ end
45
+
46
+ def escape_code code
47
+ code.mgsub! [[/&apos;/, "'"], [/&gt;/, '>'], [/&lt/, '<'], [/&quot;/, '"'], [/&amp;/, '&']]
48
+ end
49
+ end
50
+
51
+ class StringLine < CodeLine
52
+ def to_buf
53
+ " _buf << (" << escape_code(@text) << ").to_s.escape_xml.convert_newlines;"
54
+ end
55
+
56
+ def convert_newlines text
57
+ text.gsub("First line", '<text:line-break>')
58
+ end
59
+ end
60
+
61
+ class LiteralLine < CodeLine
62
+ def to_buf
63
+ " _buf << (" << escape_code(@text) << ").to_s;"
64
+ end
65
+ end
66
+
67
+ end
68
+
@@ -0,0 +1,7 @@
1
+ module Serenity
2
+ class NodeType
3
+ TAG = 1
4
+ CONTROL = 2
5
+ TEMPLATE = 3
6
+ end
7
+ end
@@ -0,0 +1,90 @@
1
+ module Serenity
2
+ class OdtEruby
3
+ include Debug
4
+
5
+ EMBEDDED_PATTERN = /\{%([=%]+)?(.*?)-?%\}/m
6
+
7
+ def initialize template
8
+ @src = convert template
9
+ if debug?
10
+ File.open(debug_file_path, 'w') do |f|
11
+ f << @src
12
+ end
13
+ end
14
+ end
15
+
16
+ def evaluate context
17
+ eval(@src, context)
18
+ end
19
+
20
+ private
21
+
22
+ def convert template
23
+ src = "_buf = '';"
24
+ buffer = []
25
+ buffer_next = []
26
+
27
+ template.each_node do |node, type|
28
+ if !buffer_next.empty?
29
+ if is_matching_pair?(buffer.last, node)
30
+ buffer.pop
31
+ next
32
+ elsif is_nonpair_tag? node
33
+ next
34
+ else
35
+ buffer << buffer_next
36
+ buffer.flatten!
37
+ buffer_next = []
38
+ end
39
+ end
40
+
41
+ if type == NodeType::CONTROL
42
+ buffer_next = process_instruction(node)
43
+ else
44
+ buffer << process_instruction(node)
45
+ buffer.flatten!
46
+ end
47
+ end
48
+
49
+ buffer.each { |line| src << line.to_buf }
50
+ src << "\n_buf.to_s\n"
51
+ end
52
+
53
+ def process_instruction text
54
+ #text = text.strip
55
+ pos = 0
56
+ src = []
57
+
58
+ text.scan(EMBEDDED_PATTERN) do |indicator, code|
59
+ m = Regexp.last_match
60
+ middle = text[pos...m.begin(0)]
61
+ pos = m.end(0)
62
+ src << Line.text(middle) unless middle.empty?
63
+
64
+ if !indicator # <% %>
65
+ src << Line.code(code)
66
+ elsif indicator == '=' # <%= %>
67
+ src << Line.string(code)
68
+ elsif indicator == '%' # <%% %>
69
+ src << Line.literal(code)
70
+ end
71
+ end
72
+
73
+ rest = pos == 0 ? text : text[pos..-1]
74
+
75
+ src << Line.text(rest) unless rest.nil? or rest.empty?
76
+ src
77
+ end
78
+
79
+ def is_nonpair_tag? tag
80
+ tag =~ /<.+?\/>/
81
+ end
82
+
83
+ def is_matching_pair? open, close
84
+ open = open.to_s.strip
85
+ close = close.to_s.strip
86
+
87
+ close == "</#{open[1, close.length - 3]}>"
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,31 @@
1
+ require 'zip/zip'
2
+ require 'fileutils'
3
+
4
+ module Serenity
5
+ class Template
6
+ attr_accessor :template
7
+
8
+ def initialize(template, output)
9
+ FileUtils.cp(template, output)
10
+ @template = output
11
+ end
12
+
13
+ def process context
14
+ tmpfiles = []
15
+ Zip::ZipFile.open(@template) do |zipfile|
16
+ %w(content.xml styles.xml).each do |xml_file|
17
+ content = zipfile.read(xml_file)
18
+ odteruby = OdtEruby.new(XmlReader.new(content))
19
+ out = odteruby.evaluate(context)
20
+ out.force_encoding Encoding.default_external.to_s
21
+
22
+ tmpfiles << (file = Tempfile.new("serenity"))
23
+ file << out
24
+ file.close
25
+
26
+ zipfile.replace(xml_file, file.path)
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,31 @@
1
+ module Serenity
2
+ class XmlReader
3
+
4
+ def initialize src
5
+ @src = src.force_encoding("UTF-8")
6
+ end
7
+
8
+ def each_node
9
+ last_match_pos = 0
10
+
11
+ @src.scan(/<.*?>/) do |node|
12
+ m = Regexp.last_match
13
+ if m.begin(0) > last_match_pos
14
+ text = @src[last_match_pos...m.begin(0)]
15
+ yield text, node_type(text) if text.gsub(/\s+/, '') != ''
16
+ end
17
+
18
+ last_match_pos = m.end(0)
19
+ yield node, NodeType::TAG
20
+ end
21
+ end
22
+
23
+ def node_type text
24
+ if text =~ /\s*\{%[^=#].+?%\}\s*/
25
+ NodeType::CONTROL
26
+ else
27
+ NodeType::TEMPLATE
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,48 @@
1
+ Sequel.migration do
2
+ change do
3
+ # Create clients table
4
+ create_table(:clients) do
5
+ primary_key :id
6
+ String :firstname
7
+ String :surname
8
+ String :company
9
+ String :address , :text => true
10
+ String :mail
11
+ Integer :rate
12
+ String :currency
13
+ end
14
+
15
+ create_table(:invoices) do
16
+ primary_key :id
17
+ foreign_key :client_id, :clients
18
+ String :name
19
+ Date :created
20
+ Date :sent
21
+ String :attributes, :text => true, :default => '{}'
22
+ end
23
+
24
+ create_table(:invoice_entries) do
25
+ primary_key :id
26
+ foreign_key :invoice_id, :invoices
27
+ String :title, :null => false
28
+ String :notes, :text => true
29
+ Date :date
30
+ String :unit, :size => 10
31
+ Float :count
32
+ Integer :cents
33
+ end
34
+
35
+ create_table(:payments) do
36
+ primary_key :id
37
+ foreign_key :invoice_id, :invoices
38
+ Integer :cents
39
+ String :note, :text => true
40
+ end
41
+
42
+ create_table(:meta) do
43
+ primary_key :id
44
+ String :key
45
+ String :value
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,283 @@
1
+ SPEC_RUNNING = true
2
+ require File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib', 'billtrap'))
3
+ require 'rspec'
4
+ require 'fakefs/safe'
5
+
6
+ module BillTrap::StubConfig
7
+ def with_stubbed_config options = {}
8
+ defaults = BillTrap::Config.defaults.dup
9
+ BillTrap::Config.stub(:[]).and_return do |k|
10
+ defaults.merge(options)[k]
11
+ end
12
+ yield if block_given?
13
+ end
14
+ end
15
+
16
+
17
+ module Helpers
18
+ def invoke command
19
+ BillTrap::CLI.args = command.shellsplit
20
+ BillTrap::CLI.invoke
21
+ end
22
+
23
+ def add_client_verified c
24
+ $stdin.should_receive(:gets).and_return(c[:firstname])
25
+ $stdin.should_receive(:gets).and_return(c[:surname])
26
+ $stdin.should_receive(:gets).and_return(c[:company])
27
+ $stdin.should_receive(:gets).and_return(c[:address])
28
+ $stdin.should_receive(:gets).and_return(c[:mail])
29
+ $stdin.should_receive(:gets).and_return(c[:rate])
30
+ $stdin.should_receive(:gets).and_return(c[:currency])
31
+ invoke 'client --add'
32
+
33
+ expect($stdout.string).to include "Client #{c[:firstname]} #{c[:surname]} was created with id"
34
+ end
35
+
36
+ def add_entry_verified e
37
+ $stdin.should_receive(:gets).and_return(e[:title])
38
+ $stdin.should_receive(:gets).and_return(e[:date])
39
+ $stdin.should_receive(:gets).and_return(e[:unit])
40
+ $stdin.should_receive(:gets).and_return(e[:count])
41
+ $stdin.should_receive(:gets).and_return(e[:price])
42
+ $stdin.should_receive(:gets).and_return(e[:notes])
43
+ invoke 'entry --add'
44
+
45
+ expect($stdout.string).to match(/Added entry \(#\d+\) to current invoice/)
46
+ end
47
+ end
48
+
49
+ RSpec.configure do |config|
50
+ config.include Helpers
51
+ config.around(:each) do |example|
52
+ # Make DBs rollback all changes
53
+ Sequel.transaction([BillTrap::DB, BillTrap::TT_DB], :rollback=>:always) do
54
+ example.run
55
+ end
56
+ end
57
+ end
58
+
59
+ describe BillTrap do
60
+ include BillTrap::StubConfig
61
+ before do
62
+ with_stubbed_config
63
+ end
64
+
65
+
66
+ before :each do
67
+ $stdout = StringIO.new
68
+ $stdin = StringIO.new
69
+ $stderr = StringIO.new
70
+ end
71
+
72
+ describe 'CLI' do
73
+ describe 'with no argument given' do
74
+ it "should display usage and exit" do
75
+ expect(lambda { invoke '' }).to raise_error SystemExit
76
+ expect($stdout.string).to include "Usage: bt COMMAND"
77
+ end
78
+ end
79
+ end
80
+
81
+ describe 'with an invalid subcommand' do
82
+ it "should output an error" do
83
+ invoke 'foozbar'
84
+ expect($stderr.string).to include 'Error: Invalid command "foozbar"'
85
+ end
86
+ end
87
+
88
+ describe 'configure' do
89
+ it "should write a config file" do
90
+ FakeFS do
91
+ billtrap_home = ENV['BILLTRAP_HOME'] || File.join(ENV['HOME'], '.billtrap')
92
+ FileUtils.mkdir_p(billtrap_home)
93
+ config_file = BillTrap::Config::CONFIG_PATH
94
+ FileUtils.rm(config_file) if File.exist? config_file
95
+ expect(File.exist?(config_file)).to be_false
96
+ invoke "configure"
97
+ expect(File.exist?(config_file)).to be_true
98
+ end
99
+ end
100
+
101
+ it "should display the path to the config file" do
102
+ FakeFS do
103
+ invoke "configure"
104
+ expect($stdout.string).to eq "Config file written to: \"#{ENV['HOME']}/.billtrap/billtrap.yml\"\n"
105
+ end
106
+ end
107
+ end
108
+
109
+ describe 'client' do
110
+ it 'should allow to add and remove clients' do
111
+
112
+ add_client_verified(
113
+ :firstname => 'John',
114
+ :surname => 'Doe',
115
+ :company => 'Doemasters Inc.',
116
+ :address => "Somestreet xyz\n 12345Sometown",
117
+ :mail => 'jdoe@example.com',
118
+ :rate => '25',
119
+ :currency => 'EUR'
120
+ )
121
+
122
+ expect(BillTrap::Client.all.size).to eq 1
123
+ expect(BillTrap::Client[1].name).to eq 'John Doe'
124
+ expect(BillTrap::Client[1].company).to eq'Doemasters Inc.'
125
+
126
+ # Assume client doesn't confirm deletion
127
+ $stdin.should_receive(:gets).and_return('n')
128
+ invoke 'client --delete 1'
129
+ expect(BillTrap::Client.all.size).to eq 1
130
+
131
+ $stdin.should_receive(:gets).and_return('y')
132
+ invoke 'client --delete 1'
133
+ expect(BillTrap::Client.all.size).to eq 0
134
+ end
135
+ end
136
+
137
+ describe 'entry' do
138
+ it 'should allow to add and delete new invoice entries' do
139
+
140
+ invoke 'new'
141
+ add_entry_verified(
142
+ :title => 'Entry1',
143
+ :date => '2013-04-10',
144
+ :count => '2',
145
+ :price => '25'
146
+ )
147
+
148
+ expect(BillTrap::InvoiceEntry.all.size).to eq 1
149
+ expect(BillTrap::Invoice.current.total).to eq Money.new('5000', 'USD')
150
+
151
+ add_entry_verified(
152
+ :title => 'Entry2',
153
+ :date => '2013-04-11',
154
+ :count => '2',
155
+ :price => '12.51'
156
+ )
157
+
158
+ expect(BillTrap::InvoiceEntry[1].title).to eq 'Entry1'
159
+ expect(BillTrap::InvoiceEntry[2].title).to eq 'Entry2'
160
+ expect(BillTrap::InvoiceEntry[1].invoice_id).to eq 1
161
+ expect(BillTrap::InvoiceEntry[2].invoice_id).to eq 1
162
+ expect(BillTrap::Invoice.current.total).to eq Money.new('7502', 'USD')
163
+
164
+ $stdin.should_receive(:gets).and_return('n')
165
+ invoke 'entry --delete 1'
166
+ expect(BillTrap::InvoiceEntry.all.size).to eq 2
167
+
168
+ $stdin.should_receive(:gets).and_return('y')
169
+ invoke 'entry --delete 1'
170
+ expect(BillTrap::InvoiceEntry[1]).to eq nil
171
+ expect(BillTrap::InvoiceEntry.all.size).to eq 1
172
+ expect(BillTrap::Invoice.current.total).to eq Money.new('2502', 'USD')
173
+ end
174
+ end
175
+
176
+ describe 'in' do
177
+ it 'should switch active invoices' do
178
+ invoke 'new --name "Some important project"'
179
+ invoke 'new --name "Support"'
180
+
181
+ expect(BillTrap::Invoice.all.size).to eq 2
182
+ expect(BillTrap::Invoice.current.name).to eq 'Support'
183
+ expect(BillTrap::Invoice.current.id).to eq 2
184
+
185
+ invoke 'in 1'
186
+ expect(BillTrap::Invoice.current.name).to eq 'Some important project'
187
+ expect(BillTrap::Invoice.current.id).to eq 1
188
+ end
189
+ end
190
+
191
+ describe 'new' do
192
+ it 'should allow to set date and client' do
193
+ add_client_verified(
194
+ :firstname => 'John',
195
+ :surname => 'Doe',
196
+ :company => 'Doemasters Inc.',
197
+ :address => "Somestreet xyz\n 12345Sometown",
198
+ :mail => 'jdoe@example.com',
199
+ :rate => '25',
200
+ :currency => 'EUR'
201
+ )
202
+
203
+ # Setting by id
204
+ invoke 'new --client 1'
205
+ expect(BillTrap::Invoice.current.client.name).to eq 'John Doe'
206
+
207
+ end
208
+ end
209
+
210
+ describe 'set' do
211
+ it 'should allow to set client for current invoice' do
212
+ add_client_verified(
213
+ :firstname => 'John',
214
+ :surname => 'Doe',
215
+ :company => 'Doemasters Inc.',
216
+ :address => "Somestreet xyz\n 12345Sometown",
217
+ :mail => 'jdoe@example.com',
218
+ :rate => '25',
219
+ :currency => 'EUR'
220
+ )
221
+
222
+ expect(BillTrap::Client.all.size).to eq 1
223
+ expect(BillTrap::Client[1].name).to eq 'John Doe'
224
+
225
+ # Create invoice
226
+ $stdout = StringIO.new
227
+ invoke 'new'
228
+ expect($stdout.string).to include "Created invoice #1"
229
+ expect(BillTrap::Invoice.current[:id]).to eq 1
230
+ invoke 'set client 1'
231
+
232
+ expect($stdout.string).to include 'SET client to John Doe (#1)'
233
+
234
+ # Expect warning output
235
+ invoke 'set client 2'
236
+ expect($stderr.string).to include "Error: Can't find Client with id '2'"
237
+ expect(BillTrap::Invoice.current.client.name).to eq 'John Doe'
238
+ end
239
+
240
+ it 'should set the current invoice name' do
241
+ invoke 'new --name "Foobar"'
242
+ expect(BillTrap::Invoice.current.name).to eq 'Foobar'
243
+
244
+ invoke 'set name "Important project"'
245
+ expect($stdout.string).to include "SET name to 'Important project'"
246
+ expect(BillTrap::Invoice.current.name).to eq 'Important project'
247
+ end
248
+
249
+ it 'should set the current invoice date' do
250
+ invoke 'new'
251
+ expect(BillTrap::Invoice.current.created).to eq Date.today
252
+
253
+ invoke 'set date 2013-04-10'
254
+ expect(BillTrap::Invoice.current.created).to eq Date.parse('2013-04-10')
255
+
256
+ # with no args sets to today
257
+ invoke 'set date'
258
+ expect(BillTrap::Invoice.current.created).to eq Date.today
259
+ end
260
+
261
+ it 'should set the current invoice sent date' do
262
+ invoke 'new'
263
+ expect(BillTrap::Invoice.current.sent).to eq nil
264
+
265
+ invoke 'set sent 2013-04-10'
266
+ expect(BillTrap::Invoice.current.sent).to eq Date.parse('2013-04-10')
267
+
268
+ invoke 'set sent'
269
+ expect(BillTrap::Invoice.current.sent).to eq nil
270
+ end
271
+
272
+ it 'should set arbitrary attributes' do
273
+ invoke 'new'
274
+ expect(BillTrap::Invoice.current.sent).to eq nil
275
+
276
+ invoke 'set foo bar'
277
+ expect(BillTrap::Invoice.current.attributes['foo']).to eq 'bar'
278
+
279
+ invoke 'set foo notbar'
280
+ expect(BillTrap::Invoice.current.attributes['foo']).to eq 'notbar'
281
+ end
282
+ end
283
+ end