wicoris-postman 0.10.0

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