billtrap 0.0.2

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