From 69936c5bd064e702f742461a99f42c19ea7e64d1 Mon Sep 17 00:00:00 2001 From: mbklein Date: Tue, 31 Mar 2009 22:56:08 +0000 Subject: [PATCH] Initial import git-svn-id: svn://svn.open-ils.org/ILS-Contrib/acq_edi/trunk@235 6d9bc8c9-1ec2-4278-b937-99fde70a366f --- Rakefile | 10 ++++ lib/edi/mapper.rb | 151 +++++++++++++++++++++++++++++++++++++++++++++++ lib/openils/mapper.rb | 99 +++++++++++++++++++++++++++++++ test/map_spec.rb | 103 ++++++++++++++++++++++++++++++++ test/openils_map_spec.rb | 39 ++++++++++++ test/test_po.json | 36 +++++++++++ 6 files changed, 438 insertions(+) create mode 100644 Rakefile create mode 100644 lib/edi/mapper.rb create mode 100644 lib/openils/mapper.rb create mode 100644 test/map_spec.rb create mode 100644 test/openils_map_spec.rb create mode 100644 test/test_po.json diff --git a/Rakefile b/Rakefile new file mode 100644 index 000000000..a7074c15c --- /dev/null +++ b/Rakefile @@ -0,0 +1,10 @@ +require 'spec/rake/spectask' + +Spec::Rake::SpecTask.new do |t| + t.ruby_opts = ['-I ./lib','-r rubygems'] + t.spec_opts = ['-c','-f specdoc'] + t.spec_files = FileList['test/**/*_spec.rb'] + t.warning = false + t.rcov = true + t.rcov_opts = ['--exclude',"json,edi4r,rcov,lib/spec,bin/spec,builder,active_"] +end \ No newline at end of file diff --git a/lib/edi/mapper.rb b/lib/edi/mapper.rb new file mode 100644 index 000000000..0ef23fcec --- /dev/null +++ b/lib/edi/mapper.rb @@ -0,0 +1,151 @@ +require 'edi4r' +require 'edi4r/edifact' +require 'json' + +class String + + def chunk(len) + re = Regexp.new(".{0,#{len.to_i}}") + self.scan(re).flatten.reject { |chunk| chunk.nil? or chunk.empty? } + end + +end + +module EDI + +module E + + class Mapper + + attr :message + attr_accessor :defaults + + class << self + def defaults + @defaults || {} + end + + def defaults=(value) + unless value.is_a?(Hash) + raise TypeError, "Mapper defaults must be in the form of a Hash" + end + @defaults = value + end + + def map(name,expr = nil,&block) + register_mapping(name,expr,block) + end + + def register_mapping(name, expr, proc) + if segment_handlers.find { |h| h[:name] == name } + raise NameError, "A pseudo-segment called '#{name}' is already registered" + end + if expr.nil? + expr = Regexp.new("^#{name}$") + end + segment_handlers.push({:name => name,:re => expr,:proc => proc}) + end + + def unregister_mapping(name) + segment_handlers.delete_if { |h| + h[:name] == name + } + end + + def find_mapping(name) + segment_handlers.find { |h| + h[:re].match(name) + } + end + + private + def segment_handlers + if @segment_handlers.nil? + @segment_handlers = [] + end + @segment_handlers + end + end + + def apply_mapping(name, value) + handler = self.class.find_mapping(name) + if handler.nil? + raise NameError, "Unknown pseudo-segment: '#{name}'" + end + handler[:proc].call(self, name, value) + end + + @segments = [] + + def self.from_json(msg_type, json, msg_opts = {}, ic_opts = {}) + result = self.new(msg_type, msg_opts, ic_opts) + result.add(JSON.parse(json)) + result + end + + def initialize(msg_type, msg_opts = {}, ic_opts = {}) + @ic = EDI::E::Interchange.new(ic_opts || {}) + @message = @ic.new_message( { :msg_type => msg_type, :version => 'D', :release => '96A', :resp_agency => 'UN' }.merge(msg_opts || {}) ) + @ic.add(@message,false) + end + + def add(*args) + if args[0].is_a?(String) + while args.length > 0 + add_segment(args.shift, args.shift) + end + elsif args.length == 1 and args[0].is_a?(Array) + add(*args[0]) + else + args.each { |arg| + add(arg) + } + end + end + + def to_s + @ic.to_s + end + + private + def add_segment(seg_name, value) + if seg_name =~ /^[A-Z]{3}$/ + seg = @message.new_segment(seg_name) + @message.add(seg) + default = self.class.defaults[seg_name] + data = default.nil? ? value : default.merge(value) + data.each_pair { |de,val| + add_element(seg,de,val,default) + } + else + apply_mapping(seg_name, value) + end + end + + def add_element(parent, de, value, default) + default = default[de] unless default.nil? + + if value.is_a?(Hash) + new_parent = parent.send("c#{de}") + data = default.nil? ? value : default.merge(value) + data.each_pair { |k,v| add_element(new_parent,k,v,default) } + elsif value.is_a?(Array) + de_array = parent.send("a#{de}") + value.each_with_index { |v,i| + element = de_array[i] + if v.is_a?(Hash) + data = default.nil? ? v : default.merge(v) + data.each_pair { |k,v1| add_element(element, k, v1, default) } + else + element.value = v + end + } + else + parent.send("d#{de}=",value) + end + end + + end + +end +end \ No newline at end of file diff --git a/lib/openils/mapper.rb b/lib/openils/mapper.rb new file mode 100644 index 000000000..24506249c --- /dev/null +++ b/lib/openils/mapper.rb @@ -0,0 +1,99 @@ +require 'active_support' +require 'edi/mapper' + +module OpenILS + + class Mapper < EDI::E::Mapper + end + +end + +OpenILS::Mapper.defaults = { + 'BGM' => { 'C002' => { '1001' => 220 }, '1225' => 9 }, + 'DTM' => { 'C507' => { '2005' => 137, '2379' => 102 } }, + 'NAD' => { 'C082' => { '3055' => '31B' } }, + 'CUX' => { 'C504' => { '6347' => 2, '6345' => 'USD', '6343' => 9 } }, + 'LIN' => { 'C212' => { '7143' => 'EN' } }, + 'PIA' => { '4347' => 5, 'C212' => { '7143' => 'IB' } }, + 'IMD' => { '7077' => 'F' }, + 'PRI' => { 'C509' => { '5125' => 'AAB' } }, + 'QTY' => { 'C186' => { '6063' => 21 } }, + 'UNS' => { '0081' => 'S' }, + 'CNT' => { 'C270' => { '6069' => 2 } } +} + +OpenILS::Mapper.map 'order' do |mapper,key,value| + mapper.add('BGM', { '1004' => value['po_number'] }) + mapper.add('DTM', { 'C507' => { '2380' => value['date'] } }) + value['buyer'].to_a.each { |buyer| mapper.add('buyer',buyer) } + value['vendor'].to_a.each { |vendor| mapper.add('vendor',vendor) } + mapper.add('currency',value['currency']) + value['items'].each_with_index { |item,index| + item['line_index'] = index + 1 + item['line_number'] = "#{value['po_number']}/#{index+1}" if item['line_number'].nil? + mapper.add('item', item) + } + mapper.add("UNS", {}) + mapper.add("CNT", { 'C270' => { '6066' => value['line_items'] } }) +end + +OpenILS::Mapper.map 'item' do |mapper,key,value| + mapper.add('LIN', { 'C212' => { '7143' => nil }, '1082' => value['line_index'] }) + id_groups = value['identifiers'].in_groups_of(5) + id_groups.each { |group| + ids = group.compact.collect { |data| + id = { '7140' => data['id'] } + if data['id-qualifier'] + id['7143'] = data['id-qualifier'] + end + id + } + mapper.add('PIA',{ 'C212' => ids }) + } + value['desc'].each { |desc| mapper.add('desc',desc) } + mapper.add('QTY', { 'C186' => { '6060' => value['quantity'] } }) + mapper.add('PRI', { 'C509' => { '5118' => value['price'] } }) + mapper.add('RFF', { 'C506' => { '1153' => 'LI', '1154' => value['line_number'] } }) +end + +OpenILS::Mapper.map('party',/^(buyer|vendor)$/) do |mapper,key,value| + codes = { 'buyer' => 'BY', 'supplier' => 'SU', 'vendor' => 'SU' } + party_code = codes[key] + + if value.is_a?(String) + value = { 'id' => value } + end + + data = { + '3035' => party_code, + 'C082' => { + '3039' => value['id'] + } + } + data['C082']['3055'] = value['id-qualifier'] unless value['id-qualifier'].nil? + mapper.add('NAD', data) + + if value['reference'] + value['reference'].each_pair { |k,v| + mapper.add('RFF', { 'C506' => { '1153' => k, '1154' => v }}) + } + end +end + +OpenILS::Mapper.map 'currency' do |mapper,key,value| + mapper.add('CUX', { 'C504' => ['6345' => value]}) +end + +OpenILS::Mapper.map 'desc' do |mapper,key,value| + values = value.to_a.flatten + while values.length > 0 + code = values.shift + text = values.shift.to_s + code_qual = code =~ /^[0-9]+$/ ? 'L' : 'F' + chunked_text = text.chunk(35) + while chunked_text.length > 0 + data = [chunked_text.shift,chunked_text.shift].compact + mapper.add('IMD', { '7077' => code_qual, '7081' => code, 'C273' => { '7008' => data } }) + end + end +end \ No newline at end of file diff --git a/test/map_spec.rb b/test/map_spec.rb new file mode 100644 index 000000000..9d925bed1 --- /dev/null +++ b/test/map_spec.rb @@ -0,0 +1,103 @@ +# map_spec.rb +require 'edi/mapper' + +describe EDI::E::Mapper do + + before(:each) do + @map = EDI::E::Mapper.new('ORDERS') + end + + it "should chunk text" do + s = 'abcdefghijklmnopqrstuvwxyz' + s.chunk(5).should == ['abcde','fghij','klmno','pqrst','uvwxy','z'] + end + + it "should produce an empty purchase order when initialized" do + ic_text = @map.to_s + ic_text.should_not be_nil + ic_text.should_not be_empty + @map.message.to_s.should == "UNH+1+ORDERS:D:96A:UN'UNT+2+1'" + end + + it "should add a single segment in tuple form" do + @map.add("BGM", {"1225" => 9,"C002" => {"1001" => 220},"1004" => "12345678"}) + @map.message.to_s.should == "UNH+1+ORDERS:D:96A:UN'BGM+220+12345678+9'UNT+3+1'" + end + + it "should properly apply defaults" do + old_defaults = EDI::E::Mapper.defaults + EDI::E::Mapper.defaults = { + 'BGM' => { 'C002' => { '1001' => 220 }, '1225' => 9 } + } + @map.add("BGM", {"1004" => "12345678"}) + EDI::E::Mapper.defaults = old_defaults + @map.message.to_s.should == "UNH+1+ORDERS:D:96A:UN'BGM+220+12345678+9'UNT+3+1'" + end + + it "should raise an exception if defaults don't look right" do + lambda { + EDI::E::Mapper.defaults = 'This is wrong!' + }.should raise_error(TypeError) + end + + it "should add multiple elements in tuple form" do + @map.add( + 'BGM', { 'C002' => { '1001' => 220 }, '1004' => '12345678', '1225' => 9 }, + 'DTM', { 'C507' => { '2005' => 137, '2380' => '20090101', '2379' => 102 }} + ) + @map.message.to_s.should == "UNH+1+ORDERS:D:96A:UN'BGM+220+12345678+9'DTM+137:20090101:102'UNT+4+1'" + end + + it "should add a single element in array form" do + @map.add(["BGM", {"1225" => 9,"C002" => {"1001" => 220},"1004" => "12345678"}]) + @map.message.to_s.should == "UNH+1+ORDERS:D:96A:UN'BGM+220+12345678+9'UNT+3+1'" + end + + it "should add multiple elements in array form" do + @map.add( + ['BGM', { 'C002' => { '1001' => 220 }, '1004' => '12345678', '1225' => 9 }], + ['DTM', { 'C507' => { '2005' => 137, '2380' => '20090101', '2379' => 102 }}] + ) + @map.message.to_s.should == "UNH+1+ORDERS:D:96A:UN'BGM+220+12345678+9'DTM+137:20090101:102'UNT+4+1'" + end + + it "should make use of custom mappings" do + EDI::E::Mapper.map 'currency' do |mapper,key,value| + mapper.add('CUX', { 'C504' => [{ '6347' => 2, '6345' => value, '6343' => 9 }]}) + end + + @map.add( + 'BGM', { 'C002' => { '1001' => 220 }, '1004' => '12345678', '1225' => 9 }, + 'DTM', { 'C507' => { '2005' => 137, '2380' => '20090101', '2379' => 102 }}, + 'currency', 'USD' + ) + @map.message.to_s.should == "UNH+1+ORDERS:D:96A:UN'BGM+220+12345678+9'DTM+137:20090101:102'CUX+2:USD:9'UNT+5+1'" + end + + it "should raise an exception when an unknown mapping is called" do + lambda { + @map.add('everything', { 'answer' => 42 }) + }.should raise_error(NameError) + end + + it "should raise an exception when re-registering a named mapping" do + lambda { + EDI::E::Mapper.map 'currency' do |mapper,key,value| + mapper.add('CUX', { 'C504' => [{ '6347' => 2, '6345' => value, '6343' => 9 }]}) + end + }.should raise_error(NameError) + end + + it "should correctly unregister a mapping" do + EDI::E::Mapper.unregister_mapping 'currency' + + lambda { + @map.add( + 'BGM', { 'C002' => { '1001' => 220 }, '1004' => '12345678', '1225' => 9 }, + 'DTM', { 'C507' => { '2005' => 137, '2380' => '20090101', '2379' => 102 }}, + 'currency', 'USD' + ) + }.should raise_error(NameError) + end + +end \ No newline at end of file diff --git a/test/openils_map_spec.rb b/test/openils_map_spec.rb new file mode 100644 index 000000000..32054cb81 --- /dev/null +++ b/test/openils_map_spec.rb @@ -0,0 +1,39 @@ +require 'openils/mapper' + +describe OpenILS::Mapper do + + before(:each) do + @map = OpenILS::Mapper.new('ORDERS') + end + + it "should add both qualified and unqualified buyer/vendor fields" do + @map.add( + ['buyer', { 'id' => '3472205', 'id-qualifier' => '91', 'reference' => { 'API' => '3472205 0001' } }], + ['buyer', { 'id' => '3472205', 'reference' => { 'API' => '3472205 0001' }}] + ) + @map.add( + 'vendor', '1556150', + 'vendor', { 'id' => '1556150', 'id-qualifier' => '91', 'reference' => { 'IA' => '1865' }} + ) + @map.message.to_s.should == "UNH+1+ORDERS:D:96A:UN'NAD+BY+3472205::91'RFF+API:3472205 0001'NAD+BY+3472205::31B'RFF+API:3472205 0001'NAD+SU+1556150::31B'NAD+SU+1556150::91'RFF+IA:1865'UNT+9+1'" + end + + it "should properly chunk and add descriptive fields" do + @map.add( + 'desc', [ + 'BAU', 'Campbell, James', + 'BTI', "The Ghost Mountain boys : their epic march and the terrifying battle for New Guinea -- the forgotten war of the South Pacific", + 'BPU', 'Crown Publishers', + 'BPD', 2007 + ] + ) + @map.message.to_s.should == "UNH+1+ORDERS:D:96A:UN'IMD+F+BAU+:::Campbell, James'IMD+F+BTI+:::The Ghost Mountain boys ?: their epi:c march and the terrifying battle f'IMD+F+BTI+:::or New Guinea -- the forgotten war :of the South Pacific'IMD+F+BPU+:::Crown Publishers'IMD+F+BPD+:::2007'UNT+7+1'" + end + + it "should create a message from JSON input" do + json = File.read(File.join(File.dirname(__FILE__), 'test_po.json')) + @map = OpenILS::Mapper.from_json('ORDERS',json) + @map.message.to_s.should == "UNH+1+ORDERS:D:96A:UN'BGM+220+2+9'DTM+137:20090331:102'NAD+BY+3472205::91'RFF+API:3472205 0001'NAD+BY+3472205::31B'RFF+API:3472205 0001'NAD+SU+1556150::31B'NAD+SU+1556150::91'RFF+IA:1865'CUX+2:USD:9'LIN+1'PIA+5+03-0010837:SA'IMD+F+BTI+:::Discernment'IMD+F+BPU+:::Concord Records,'IMD+F+BPD+:::1986.'IMD+F+BPH+:::1 sound disc ?:'QTY+21:2'PRI+AAB:35.95'RFF+LI:2/1'LIN+2'PIA+5+03-0010840:SA'IMD+F+BTI+:::The inner source'IMD+F+BAU+:::Duke, George, 1946-'IMD+F+BPU+:::MPS Records,'IMD+F+BPD+:::1973.'IMD+F+BPH+:::2 sound discs ?:'QTY+21:1'PRI+AAB:28.95'RFF+LI:2/2'UNS+S'CNT+2:2'UNT+33+1'" + end + +end \ No newline at end of file diff --git a/test/test_po.json b/test/test_po.json new file mode 100644 index 000000000..802409623 --- /dev/null +++ b/test/test_po.json @@ -0,0 +1,36 @@ +["order", { + "po_number":2, + "date":"20090331", + "buyer":[ + {"id-qualifier":"91","id":"3472205","reference":{"API":"3472205 0001"}}, + {"id":"3472205","reference":{"API":"3472205 0001"}} + ], + "vendor":[ + "1556150", + {"id-qualifier":"91","reference":{"IA":"1865"},"id":"1556150"} + ], + "currency":"USD", + "items":[{ + "identifiers":[{"id-qualifier":"SA","id":"03-0010837"}], + "price":35.95, + "desc":[ + {"BTI":"Discernment"}, + {"BPU":"Concord Records,"}, + {"BPD":"1986."}, + {"BPH":"1 sound disc :"} + ], + "quantity":2 + },{ + "identifiers":[{"id-qualifier":"SA","id":"03-0010840"}], + "price":28.95, + "desc":[ + {"BTI":"The inner source"}, + {"BAU":"Duke, George, 1946-"}, + {"BPU":"MPS Records,"}, + {"BPD":"1973."}, + {"BPH":"2 sound discs :"} + ], + "quantity":1 + }], + "line_items":2 +}] \ No newline at end of file -- 2.11.0