wicoris-postman 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,19 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .rbenv-version
6
+ .yardoc
7
+ Gemfile.lock
8
+ InstalledFiles
9
+ _yardoc
10
+ coverage
11
+ doc/
12
+ lib/bundler/man
13
+ pkg
14
+ rdoc
15
+ spec/reports
16
+ test/tmp
17
+ test/version_tmp
18
+ tmp
19
+ vendor
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in wicoris-postman.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Björn Albers
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,38 @@
1
+ # WiCoRIS-Postman
2
+
3
+ Deliver letters from WiCoRIS
4
+
5
+
6
+ ## Installation
7
+
8
+ $ gem install wicoris-postman
9
+
10
+ (NOTE: You might have to prefix this with `sudo`.)
11
+
12
+
13
+ ## Usage
14
+
15
+ Just parse JSON files and see if the right command-lines would be
16
+ executed:
17
+
18
+ $ postman /Library/FileMaker\ Server/Data/Documents --noop
19
+
20
+ The real shit (actually fax some letters and trash the JSON-files):
21
+
22
+ $ postman /Library/FileMaker\ Server/Data/Documents
23
+
24
+
25
+ ## Contribution
26
+
27
+ ### Bootstraping
28
+
29
+ $ bundle install --path vendor
30
+
31
+ ### Testing
32
+
33
+ $ bundle exec cucumber
34
+
35
+
36
+ ## Copyright
37
+
38
+ Copyright (c) 2014 Björn Albers
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
data/bin/postman ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ # WiCoRIS-Postman Prototype
3
+
4
+ require 'wicoris/postman'
5
+ include Wicoris::Postman
6
+
7
+ CLI.new.run
@@ -0,0 +1,30 @@
1
+ Feature: Copy letter
2
+
3
+ In order to save money, time and trees
4
+ WiCoRIS-Postman should deliver letters by copying them to shared folder
5
+
6
+ Background:
7
+ Given a letter
8
+ And a copy job
9
+ And an output folder
10
+
11
+ @announce
12
+ Scenario: Single copy job
13
+ When I run postman with output dir
14
+ Then the letter should have been copied
15
+ And the job should have been deleted
16
+ And the copy should have been logged
17
+
18
+ @announce
19
+ Scenario: No copy in NOOP mode
20
+ When I run postman with output dir in noop mode
21
+ Then the letter should not have been copied
22
+ And the job should not have been deleted
23
+ And the copy should have been logged
24
+
25
+ @announce
26
+ Scenario: Run with config file
27
+ When I run postman with output dir from config file
28
+ Then the letter should have been copied
29
+ And the job should have been deleted
30
+ And the copy should have been logged
@@ -0,0 +1,23 @@
1
+ Feature: Fax letter
2
+
3
+ In order to save money, time and trees
4
+ WiCoRIS-Postman should deliver letters by fax
5
+
6
+ Background:
7
+ Given a letter
8
+ And a fax job
9
+ And a doubled fax interface
10
+
11
+ @announce
12
+ Scenario: Single fax job
13
+ When I run postman
14
+ Then the letter should have been faxed
15
+ And the job should have been deleted
16
+ And the fax should have been logged
17
+
18
+ @announce
19
+ Scenario: NOOP mode
20
+ When I run postman in noop mode
21
+ Then the letter should not have been faxed
22
+ And the job should not have been deleted
23
+ And the fax should have been logged
@@ -0,0 +1,60 @@
1
+ Given(/^a copy job$/) do
2
+ @job_dir = 'Documents'
3
+ @job = File.join(@job_dir, 'copy.json')
4
+ content = {
5
+ 'type' => 'copy',
6
+ 'file' => @letter,
7
+ 'recipient' => 'Dr. Hasenbein',
8
+ 'patient_first_name' => 'Chuck',
9
+ 'patient_last_name' => 'Norris',
10
+ 'patient_date_of_birth' => '1940-03-10'
11
+ }
12
+ create_dir(@job_dir)
13
+ write_file(@job, content.to_json)
14
+ end
15
+
16
+ Given(/^an output folder$/) do
17
+ @output_dir = 'export'
18
+ create_dir(@output_dir)
19
+ end
20
+
21
+
22
+ When(/^I run postman with output dir$/) do
23
+ cmd = "postman --jobdir Documents --outdir '#{@output_dir}'"
24
+ run_simple(unescape(cmd))
25
+ end
26
+
27
+ When(/^I run postman with output dir in noop mode$/) do
28
+ cmd = "postman --jobdir Documents --outdir '#{@output_dir}' --noop"
29
+ run_simple(unescape(cmd))
30
+ end
31
+
32
+ When(/^I run postman with output dir from config file$/) do
33
+ content = ''
34
+ content << "jobdir 'Documents'\n"
35
+ content << "outdir '#{@output_dir}'\n"
36
+ write_file('config.rb', content)
37
+ cmd = "postman --config 'config.rb'"
38
+ run_simple(unescape(cmd))
39
+ end
40
+
41
+
42
+ Then(/^the letter should have been copied$/) do
43
+ @output_file = 'Norris_Chuck_1940-03-10_792e.pdf' # Output file with fingerprint
44
+ copied_letter = File.join(@output_dir, @output_file)
45
+ check_file_presence([copied_letter], true)
46
+ end
47
+
48
+ Then(/^the letter should not have been copied$/) do
49
+ @output_file = 'Norris_Chuck_1940-03-10_792e.pdf' # Output file with fingerprint
50
+ copied_letter = File.join(@output_dir, File.basename(@letter))
51
+ check_file_presence([copied_letter], false)
52
+ end
53
+
54
+ Then(/^the copy should have been logged$/) do
55
+ [
56
+ 'Letter delivered',
57
+ @job,
58
+ @output_file
59
+ ].each { |expected| assert_partial_output(expected, all_output) }
60
+ end
@@ -0,0 +1,45 @@
1
+ Given(/^a fax job$/) do
2
+ dir = 'Documents'
3
+ @job = File.join(dir, 'example.json')
4
+ content = {
5
+ 'type' => 'fax',
6
+ 'file' => @letter,
7
+ 'phone' => '0123456789'
8
+ }
9
+ create_dir(dir)
10
+ write_file(@job, content.to_json)
11
+ end
12
+
13
+ Given(/^a doubled fax interface$/) do
14
+ double_cmd('lp')
15
+ end
16
+
17
+ When(/^I run postman$/) do
18
+ cmd = 'postman --jobdir Documents'
19
+ run_simple(unescape(cmd))
20
+ end
21
+
22
+ When(/^I run postman in noop mode$/) do
23
+ cmd = 'postman --jobdir Documents --noop'
24
+ run_simple(unescape(cmd))
25
+ end
26
+
27
+ def fax_cmd
28
+ cmd = "lp -d Fax -o phone=00123456789 \"#{@letter}\"".shellsplit
29
+ end
30
+
31
+ Then(/^the letter should have been faxed$/) do
32
+ expect(history).to include(fax_cmd), history.to_pretty
33
+ end
34
+
35
+ Then(/^the letter should not have been faxed$/) do
36
+ expect(history).to_not include(fax_cmd), history.to_pretty
37
+ end
38
+
39
+ Then(/^the fax should have been logged$/) do
40
+ [
41
+ 'Letter delivered',
42
+ @job,
43
+ '0123456789'
44
+ ].each { |expected| assert_partial_output(expected, all_output) }
45
+ end
@@ -0,0 +1,11 @@
1
+ Given(/^a letter$/) do
2
+ @letter = write_file('document.pdf', 'chunky bacon').path
3
+ end
4
+
5
+ Then(/^the job should have been deleted$/) do
6
+ check_file_presence([@job], false)
7
+ end
8
+
9
+ Then(/^the job should not have been deleted$/) do
10
+ check_file_presence([@job], true)
11
+ end
@@ -0,0 +1,2 @@
1
+ require 'aruba/cucumber'
2
+ require 'aruba-doubles/cucumber'
@@ -0,0 +1,44 @@
1
+ require 'time'
2
+ module Wicoris
3
+ module Postman
4
+ class CLI
5
+ include Mixlib::CLI
6
+
7
+ option :jobdir,
8
+ :long => '--jobdir DIRECTORY',
9
+ :short => '-j',
10
+ :description => 'Directory with all JSON files a.k.a. jobs'
11
+
12
+ option :noop,
13
+ :long => '--noop',
14
+ :short => '-n',
15
+ :description => "Don't do anything, just pretend to",
16
+ :boolean => true,
17
+ :default => false
18
+
19
+ option :outdir,
20
+ :long => '--outdir DIRECTORY',
21
+ :short => '-o',
22
+ :description => 'Output directory for copying letters'
23
+
24
+ option :config,
25
+ :long => '--config CONFIG_FILE',
26
+ :short => '-c',
27
+ :description => 'Configuration file for Postman'
28
+
29
+ # Run a postman that clears the mailbox.
30
+ def run
31
+ Postman.new(opts).run
32
+ end
33
+
34
+ private
35
+
36
+ # @returns [Hash] App config
37
+ def opts
38
+ parse_options
39
+ Config.from_file(config[:config]) if config[:config]
40
+ Config.merge!(config)
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,79 @@
1
+ require 'fileutils'
2
+ require 'digest/md5'
3
+
4
+ module Wicoris
5
+ module Postman
6
+ class Copier
7
+ def initialize(job, opts = {})
8
+ @job = job
9
+ @opts = opts
10
+ @logger = opts[:logger]
11
+ end
12
+
13
+ # Copy letter to destination.
14
+ def run
15
+ FileUtils.cp(source, destination, :noop => (@opts[:noop] == true))
16
+ if @logger
17
+ msg = @job.to_hash
18
+ msg[:destination] = destination
19
+ msg[:message] = 'Letter delivered :-)'
20
+ @logger.info(msg)
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ # @returns [String] Input filename
27
+ def source
28
+ @job.letter
29
+ end
30
+
31
+ # @returns [String] Simple fingerprint of input file.
32
+ def fingerprint
33
+ Digest::MD5.hexdigest(File.read(source))[0..3]
34
+ end
35
+
36
+ # @returns [String] Full path to output file
37
+ def destination
38
+ File.join(outdir, filename)
39
+ end
40
+
41
+ # @returns [String] Validated output directory
42
+ def outdir
43
+ dir = @opts[:outdir]
44
+ raise 'No output directory given' unless dir
45
+ raise "Output directory does not exist: #{dir}" unless
46
+ File.exists? dir
47
+ raise "Output directory is no directory: '#{dir}'" unless
48
+ File.directory? dir
49
+ raise "Output directory not writable: '#{dir}'" unless
50
+ File.writable? dir
51
+ @opts[:outdir]
52
+ end
53
+
54
+ # @returns [String] Output filename
55
+ def filename
56
+ if filename_components.any? { |c| c.nil? || c.empty? }
57
+ raise "Missing patient demographics: #{filename_components}"
58
+ else
59
+ filename_components.join('_') + suffix
60
+ end
61
+ end
62
+
63
+ # @returns [Array] Infos to be included in the output filename
64
+ def filename_components
65
+ [
66
+ @job.patient_last_name,
67
+ @job.patient_first_name,
68
+ @job.patient_date_of_birth,
69
+ fingerprint
70
+ ]
71
+ end
72
+
73
+ # @returns [String] Suffix for output filename
74
+ def suffix
75
+ '.pdf'
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,53 @@
1
+ module Wicoris
2
+ module Postman
3
+ class FaxMachine
4
+ DIALOUT_PREFIX = '0'
5
+ VALID_PHONE_NUMBER = %r{
6
+ ^0 # starts with a zero
7
+ [1-9]{1} # but not with a second zero
8
+ \d{8,}$ # followed by at least 8 digits
9
+ }x
10
+
11
+ def initialize(job, opts = {})
12
+ @job = job
13
+ @opts = opts
14
+ @logger = opts[:logger]
15
+ end
16
+
17
+ # Actually fax the letter.
18
+ def run
19
+ system(command) unless @opts[:noop]
20
+ if @logger
21
+ msg = @job.to_hash
22
+ msg[:message] = 'Letter delivered :-)'
23
+ @logger.info(msg)
24
+ end
25
+ end
26
+
27
+ # @returns [String] Validated phone number.
28
+ def validated_phone
29
+ raise ArgumentError, 'Missing phone number' unless @job.phone
30
+ phone = @job.phone.gsub(/(\s|-)+/, '')
31
+ if phone =~ VALID_PHONE_NUMBER
32
+ DIALOUT_PREFIX + phone
33
+ else
34
+ raise ArgumentError, "Invalid phone number: #{phone}"
35
+ end
36
+ end
37
+
38
+ private
39
+
40
+ # Return the command-line for sending the fax, i.e.:
41
+ #
42
+ # lp -d Fax -o phone=042 "/tmp/foo.pdf"
43
+ #
44
+ # @returns [String] command-line
45
+ def command
46
+ cmd = %w(lp -d Fax) # TODO: Replace hard-coded fax printer-name 'Fax'!
47
+ cmd << "-o phone=#{validated_phone}"
48
+ cmd << "'#{@job.letter}'"
49
+ cmd.join(' ')
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,77 @@
1
+ module Wicoris
2
+ module Postman
3
+ class Job
4
+ attr_reader :logger
5
+
6
+ def initialize(json_file, opts = {})
7
+ @json_file = json_file
8
+ @opts = opts
9
+ @logger = opts[:logger]
10
+ end
11
+
12
+ # @returns [String] Path to actual letter
13
+ def letter
14
+ if not File.exists?(file)
15
+ msg = to_hash
16
+ msg[:message] = 'Letter does not exist'
17
+ @opts[:logger].error(msg) if @opts[:logger]
18
+ raise "Letter does not exist: #{file}" # TODO: DRY!
19
+ else
20
+ file
21
+ end
22
+ end
23
+
24
+ # Process the job
25
+ def process
26
+ # NOTE: `Object#type` is an existing method in Ruby 1.8.7, therefore we
27
+ # have to fetch the attribute from the JSON hash.
28
+ delivery_method =
29
+ case json['type']
30
+ when 'fax' then FaxMachine
31
+ when 'copy' then Copier
32
+ # TODO: Handle unknown case!
33
+ #else
34
+ # ...
35
+ end
36
+ delivery_method.new(self, @opts).run
37
+ end
38
+
39
+ # Remove the JSON file.
40
+ def clear!
41
+ FileUtils.rm(@json_file, :noop => (@opts[:noop] == true)) if json
42
+ rescue JSON::ParserError
43
+ logger.warn :message => 'Refused to delete non-JSON file.',
44
+ :json_file => @json_file
45
+ end
46
+
47
+ # @returns [Hash] Job properties
48
+ def to_hash
49
+ properties = { 'json_file' => @json_file }
50
+ properties.merge(json)
51
+ rescue
52
+ properties
53
+ end
54
+
55
+ private
56
+
57
+ # Parse and return the JSON.
58
+ # @returns [Hash] Cached JSON.
59
+ def json
60
+ @json ||= JSON.parse(json_file_content)
61
+ end
62
+
63
+ # FileMaker creates JSON files with Mac OS Roman file encoding.
64
+ # We have to convert it to UTF-8 in order to avoid cryptic symbols.
65
+ #
66
+ # @returns [String] UTF-8 encoded file content.
67
+ def json_file_content
68
+ Iconv.conv('UTF-8', 'MacRoman', File.read(@json_file))
69
+ end
70
+
71
+ # Provide convenient methods for accessing JSON attributes.
72
+ def method_missing(id,*args,&block)
73
+ json.key?(id.to_s) ? json[id.to_s] : super
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,27 @@
1
+ module Wicoris
2
+ module Postman
3
+ class Logger
4
+ def initialize
5
+ STDOUT.sync = true
6
+ end
7
+
8
+ def info(msg)
9
+ event = msg.merge(
10
+ :level => :info,
11
+ :timestamp => Time.now.iso8601
12
+ )
13
+ puts event.to_json
14
+ rescue JSON::ParserError
15
+ puts event.inspect
16
+ end
17
+
18
+ def error(msg)
19
+ event = msg.merge(
20
+ :level => :error,
21
+ :timestamp => Time.now.iso8601
22
+ )
23
+ puts event.to_json
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,41 @@
1
+ module Wicoris
2
+ module Postman
3
+ class Postman
4
+ def initialize(opts = {})
5
+ @opts = opts
6
+ @logger = opts[:logger]
7
+ end
8
+
9
+ # Process each job.
10
+ def run
11
+ jobs.each do |job|
12
+ begin
13
+ job.process
14
+ rescue => e
15
+ @logger.error(e) if @logger
16
+ ensure
17
+ job.clear!
18
+ end
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ # @returns [Array<Job>] All jobs created each JSON file
25
+ def jobs
26
+ json_files.map { |f| Job.new(f, @opts) }
27
+ end
28
+
29
+ # @returns [Array<String>] JSON files in jobdir.
30
+ def json_files
31
+ # NOTE: This performs case-insensitive globbing.
32
+ Dir.glob(File.join(jobdir, '*.JSON'), File::FNM_CASEFOLD)
33
+ end
34
+
35
+ # @returns [String] Path to jobdir directory
36
+ def jobdir
37
+ @opts[:jobdir]
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,5 @@
1
+ module Wicoris
2
+ module Postman
3
+ VERSION = '0.10.0'
4
+ end
5
+ end
@@ -0,0 +1,22 @@
1
+ require 'cabin'
2
+ require 'json'
3
+ require 'mixlib/cli'
4
+ require 'mixlib/config'
5
+ require 'jlo'
6
+ require 'wicoris/postman/version'
7
+ require 'wicoris/postman/logger'
8
+ require 'wicoris/postman/job'
9
+ require 'wicoris/postman/fax_machine'
10
+ require 'wicoris/postman/copier'
11
+ require 'wicoris/postman/postman'
12
+ require 'wicoris/postman/cli'
13
+
14
+ module Wicoris
15
+ module Postman
16
+ class Config
17
+ extend(Mixlib::Config)
18
+
19
+ default :logger, (l = ::Logger.new(STDOUT); l.formatter = ::JLo::JSONEvent; l)
20
+ end
21
+ end
22
+ end
@@ -0,0 +1 @@
1
+ require 'wicoris/postman'
@@ -0,0 +1,40 @@
1
+ require 'spec_helper'
2
+
3
+ module Wicoris::Postman
4
+ describe CLI do
5
+ let(:cli) { CLI.new }
6
+
7
+ describe '#run' do
8
+ it 'runs a new postman with options' do
9
+ postman = double('postman')
10
+ opts = double('config')
11
+ cli.should_receive(:opts).and_return(opts)
12
+ Postman.should_receive(:new).ordered.with(opts).and_return(postman)
13
+ postman.should_receive(:run)
14
+ cli.run
15
+ end
16
+ end
17
+
18
+ describe '#opts' do
19
+ it 'returns the application-wide options' do
20
+ cli.should_receive(:parse_options).ordered
21
+ expect(cli.send(:opts)).to eq(Config)
22
+ end
23
+
24
+ it 'reads a given config file'
25
+
26
+ it 'merges command-line opts with the global config'
27
+ end
28
+
29
+ describe '#logger' do
30
+ let(:logger) { double('logger') }
31
+
32
+ it 'returns a new ruby-cabin logger' do
33
+ pending 'logging has changed'
34
+ Cabin::Channel.should_receive(:new).and_return(logger)
35
+ logger.should_receive(:subscribe).with(STDOUT)
36
+ expect(cli.send(:logger)).to eq logger
37
+ end
38
+ end
39
+ end
40
+ end