openils-mapper 0.8.2

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc ADDED
@@ -0,0 +1,60 @@
1
+ = Introduction
2
+
3
+ This module adds methods to edi4r's EDI::E::Interchange class to map between EDIFACT, Ruby structures, and JSON strings. It takes JSON input and turns it into valid, well-formatted EDIFACT messages. In order to provide the greatest range of flexibility, the mapper can process two types of input interchangeably: low-level (or raw) and high-level.
4
+
5
+ == Low-Level
6
+
7
+ Low-level input describes the exact structure of the required EDIFACT segments and data elements, without having to worry about syntax or exact placement. Any segment, simple data element (DE), or composite data element (CDE) can be specified directly. The tradeoff, of course, is that the application sending the JSON message has to be intimately aware of the underlying EDIFACT message's structure, including all segment and DE/CDE order, data types, and code lists. For example, the following EDIFACT segments describe a vendor (SU) identified by its Standard Address Number (code 31B), and with an internal department code (1865, qualified by the code IA):
8
+
9
+ NAD+SU+1556150::31B'
10
+ RFF+IA:1865'
11
+
12
+ The low-level JSON representation of those two segments looks like this:
13
+
14
+ [
15
+ 'NAD', { '3035' : 'SU', 'C082' : { '3039' : '1556150', '3055' : '31B' }},
16
+ 'RFF', { 'C506' : { '1153' : 'IA', '1154' : '1865' }}
17
+ ]
18
+
19
+ In this case, the JSON is more verbose than the raw EDIFACT, but at least it insulates the caller from the details of element order, blank DEs, and character escaping.
20
+
21
+ == High-Level
22
+
23
+ The mapper's high-level mode defines a number of pseudo-segments that know how to translate themselves into their low-level counterparts. For example, to create a simple supplier segment with an ID and using the defaults for everything else:
24
+
25
+ ['vendor', '1556150']
26
+ => NAD+SU+1556150::91'
27
+
28
+ But it's easy to override some of the defaults and add extra segment information as well:
29
+
30
+ ['vendor', { 'id' : '1556150', 'id-qual' : '31B', 'reference' : { 'IA' : '1865' }}]
31
+ => NAD+SU+1556150::31B'
32
+ RFF+IA:1865'
33
+
34
+ Another example involves the descriptive metadata sent with each line item in a purchase order. Strings longer than 35 characters have to be split across multiple DEs, and strings longer than 70 characters require multiple iterations of the IMD segment:
35
+
36
+ IMD+F+BAU+:::Campbell, James'
37
+ IMD+F+BTI+:::The Ghost Mountain boys ?: their epi:c march and the terrifying battle f'
38
+ IMD+F+BTI+:::or New Guinea -- the forgotten war :of the South Pacific'
39
+ IMD+F+BPU+:::Crown Publishers'
40
+ IMD+F+BPD+:::2007'''
41
+
42
+ Note the character escaping (?:) and segmentation (:), as well as the repetition of the IMD title (BTI) field to accommodate the long string. The high level version can look like this:
43
+
44
+ [
45
+ 'desc', { 'BAU' : 'Campbell, James' },
46
+ 'desc', { 'BTI' : "The Ghost Mountain boys : their epic march and the terrifying battle for New Guinea -- the forgotten war of the South Pacific" },
47
+ 'desc', { 'BPU' : 'Crown Publishers' },
48
+ 'desc', { 'BPD' : 2007 }
49
+ ]
50
+
51
+ or even like this:
52
+
53
+ ['desc', [
54
+ 'BAU', 'Campbell, James',
55
+ 'BTI', "The Ghost Mountain boys : their epic march and the terrifying battle for New Guinea -- the forgotten war of the South Pacific",
56
+ 'BPU', 'Crown Publishers',
57
+ 'BPD', 2007
58
+ ]]
59
+
60
+ Since the mapper uses regular expressions and closures to define and evaluate the high-level pseudo-segments, the mapper can be extended very easily (in some cases, trivially), and with great flexibility.
data/Rakefile ADDED
@@ -0,0 +1,11 @@
1
+ require 'rubygems'
2
+ require 'spec/rake/spectask'
3
+
4
+ Spec::Rake::SpecTask.new do |t|
5
+ t.ruby_opts = ['-I ./lib','-r rubygems']
6
+ t.spec_opts = ['-c','-f specdoc']
7
+ t.spec_files = FileList['test/**/*_spec.rb']
8
+ t.warning = false
9
+ t.rcov = true
10
+ t.rcov_opts = ['--exclude',"json,edi4r,rcov,lib/spec,bin/spec,builder,active_"]
11
+ end
@@ -0,0 +1,76 @@
1
+ require 'json'
2
+
3
+ module EDI
4
+
5
+ class Collection
6
+ include Enumerable
7
+
8
+ def to_hash
9
+ result = {}
10
+
11
+ self.each { |child|
12
+ if child.is_a?(Collection)
13
+ hash = child.to_hash
14
+ result[child.name] = hash unless hash.empty?
15
+ unless self.children.empty?
16
+ segments = []
17
+ self.children.each { |segment|
18
+ segments << [segment.name, segment.to_hash]
19
+ }
20
+ result[self.sg_name] = segments
21
+ end
22
+ else
23
+ unless child.value.nil?
24
+ result[child.name] = child.value
25
+ end
26
+ end
27
+ }
28
+ result
29
+ end
30
+
31
+ def to_json(*a)
32
+ self.to_hash.to_json(*a)
33
+ end
34
+
35
+ end
36
+
37
+ class Interchange
38
+
39
+ def to_hash
40
+
41
+ messages = []
42
+ self.each { |message|
43
+ messages << [message.name, message.to_hash]
44
+ }
45
+
46
+ {
47
+ 'UNA' => self.una.to_s,
48
+ 'header' => [self.header.name, self.header.to_hash],
49
+ 'body' => messages,
50
+ 'trailer' => [self.trailer.name, self.trailer.to_hash]
51
+ }
52
+ end
53
+
54
+ end
55
+
56
+ class Message
57
+
58
+ def to_hash
59
+ segments = []
60
+
61
+ self.find_all { |segment|
62
+ segment.level < 2
63
+ }.each { |segment|
64
+ segments << [segment.name, segment.to_hash]
65
+ }
66
+ segments << [self.trailer.name, self.trailer.to_hash]
67
+ {
68
+ 'header' => [self.header.name, self.header.to_hash],
69
+ 'body' => segments,
70
+ 'trailer' => [self.trailer.name, self.trailer.to_hash]
71
+ }
72
+ end
73
+
74
+ end
75
+
76
+ end
data/lib/edi/mapper.rb ADDED
@@ -0,0 +1,209 @@
1
+ require 'edi4r'
2
+ require 'edi4r/edifact'
3
+ begin
4
+ require 'edi4r-tdid'
5
+ rescue LoadError
6
+ warn "WARNING: edi4r-tdid not found. Only EDIFACT versions d96a and d01b will be supported!"
7
+ end
8
+ require 'forwardable'
9
+ require 'json'
10
+
11
+ class String
12
+
13
+ def chunk(len)
14
+ re = Regexp.new(".{0,#{len.to_i}}")
15
+ self.scan(re).flatten.reject { |chunk| chunk.nil? or chunk.empty? }
16
+ end
17
+
18
+ end
19
+
20
+ module EDI::E
21
+
22
+ class Mapper
23
+ extend Forwardable
24
+
25
+ attr :message
26
+ attr_accessor :defaults
27
+ def_delegators :@ic, :charset, :empty?, :groups_created, :header,
28
+ :illegal_charset_pattern, :inspect, :is_iedi?, :messages_created,
29
+ :output_mode, :output_mode=, :show_una, :show_una=, :syntax, :to_s,
30
+ :to_xml, :to_xml_header, :to_xml_trailer, :trailer, :una, :validate,
31
+ :version
32
+
33
+ class << self
34
+ def defaults
35
+ @defaults || {}
36
+ end
37
+
38
+ def defaults=(value)
39
+ unless value.is_a?(Hash)
40
+ raise TypeError, "Mapper defaults must be in the form of a Hash"
41
+ end
42
+ @defaults = value
43
+ end
44
+
45
+ def map(name,expr = nil,&block)
46
+ register_mapping(name,expr,block)
47
+ end
48
+
49
+ def register_mapping(name, expr, proc)
50
+ if segment_handlers.find { |h| h[:name] == name }
51
+ raise NameError, "A pseudo-segment called '#{name}' is already registered"
52
+ end
53
+ if expr.nil?
54
+ expr = Regexp.new("^#{name}$")
55
+ end
56
+ segment_handlers.push({:name => name,:re => expr,:proc => proc})
57
+ end
58
+
59
+ def unregister_mapping(name)
60
+ segment_handlers.delete_if { |h|
61
+ h[:name] == name
62
+ }
63
+ end
64
+
65
+ def find_mapping(name)
66
+ segment_handlers.find { |h|
67
+ h[:re].match(name)
68
+ }
69
+ end
70
+
71
+ private
72
+ def segment_handlers
73
+ if @segment_handlers.nil?
74
+ @segment_handlers = []
75
+ end
76
+ @segment_handlers
77
+ end
78
+ end
79
+
80
+ def apply_mapping(name, value)
81
+ handler = self.class.find_mapping(name)
82
+ if handler.nil?
83
+ raise NameError, "Unknown pseudo-segment: '#{name}'"
84
+ end
85
+ handler[:proc].call(self, name, value)
86
+ end
87
+
88
+ def self.from_json(json, ic_opts = {})
89
+ struct = JSON.parse(json)
90
+
91
+ json_opts = {}
92
+ [:sender,:recipient].each { |party|
93
+ party_info = struct[party.to_s]
94
+ if party_info.is_a?(Hash)
95
+ json_opts[party] = party_info['id']
96
+ json_opts["#{party}_qual".to_sym] = party_info['id-qualifier']
97
+ elsif party_info.is_a?(String)
98
+ (id,qual) = party_info.split(/:/)
99
+ json_opts[party] = id
100
+ json_opts["#{party}_qual".to_sym] = qual
101
+ end
102
+ }
103
+
104
+ json_msg_opts = {}
105
+ if struct['msg_opts'].is_a?(Hash)
106
+ struct['msg_opts'].each_pair { |k,v| json_msg_opts[k.to_sym] = v }
107
+ end
108
+
109
+ result = self.new(struct['msg_type'], json_msg_opts, ic_opts.merge(json_opts))
110
+ result.add(struct['msg'])
111
+ result.finalize
112
+ end
113
+
114
+ def initialize(msg_type, msg_opts = {}, ic_opts = {})
115
+ # Bug in edi4r 0.9 -- sometimes :recipient is used; sometimes :recip. It doesn't
116
+ # work. We'll override it.
117
+ local_ic_opts = ic_opts.reject { |k,v| [:sender,:sender_qual,:recipient,:recipient_qual].include?(k) }
118
+ @ic = EDI::E::Interchange.new(local_ic_opts || {})
119
+
120
+ # Apply any envelope defaults.
121
+ ['UNA','UNB','UNZ'].each { |seg|
122
+ seg_defs = self.class.defaults[seg]
123
+ if seg_defs
124
+ seg_defs.each_pair { |cde,defs|
125
+ segment = @ic.header[cde].first
126
+ unless segment.nil?
127
+ defs.each_pair { |de,val|
128
+ segment[de][0].value = val
129
+ }
130
+ end
131
+ }
132
+ end
133
+ }
134
+
135
+ @ic.header.cS002.d0004 = ic_opts[:sender] unless ic_opts[:sender].nil?
136
+ @ic.header.cS002.d0007 = ic_opts[:sender_qual] unless ic_opts[:sender_qual].nil?
137
+ @ic.header.cS003.d0010 = ic_opts[:recipient] unless ic_opts[:recipient].nil?
138
+ @ic.header.cS003.d0007 = ic_opts[:recipient_qual] unless ic_opts[:recipient_qual].nil?
139
+
140
+ @message = @ic.new_message( { :msg_type => msg_type, :version => 'D', :release => '96A', :resp_agency => 'UN' }.merge(msg_opts || {}) )
141
+ @ic.add(@message,false)
142
+ end
143
+
144
+ def add(*args)
145
+ if args[0].is_a?(String)
146
+ while args.length > 0
147
+ add_segment(args.shift, args.shift)
148
+ end
149
+ elsif args.length == 1 and args[0].is_a?(Array)
150
+ add(*args[0])
151
+ else
152
+ args.each { |arg|
153
+ add(arg)
154
+ }
155
+ end
156
+ end
157
+
158
+ def finalize
159
+ mode = @ic.output_mode
160
+ @ic = EDI::E::Interchange.parse(StringIO.new(@ic.to_s))
161
+ @ic.output_mode = mode
162
+ return self
163
+ end
164
+
165
+ def to_s
166
+ @ic.to_s
167
+ end
168
+
169
+ private
170
+ def add_segment(seg_name, value)
171
+ if seg_name =~ /^[A-Z]{3}$/
172
+ seg = @message.new_segment(seg_name)
173
+ @message.add(seg)
174
+ default = self.class.defaults[seg_name]
175
+ data = default.nil? ? value : default.merge(value)
176
+ data.each_pair { |de,val|
177
+ add_element(seg,de,val,default)
178
+ }
179
+ else
180
+ apply_mapping(seg_name, value)
181
+ end
182
+ end
183
+
184
+ def add_element(parent, de, value, default)
185
+ default = default[de] unless default.nil?
186
+
187
+ if value.is_a?(Hash)
188
+ new_parent = parent.send("c#{de}")
189
+ data = default.nil? ? value : default.merge(value)
190
+ data.each_pair { |k,v| add_element(new_parent,k,v,default) }
191
+ elsif value.is_a?(Array)
192
+ de_array = parent.send("a#{de}")
193
+ value.each_with_index { |v,i|
194
+ element = de_array[i]
195
+ if v.is_a?(Hash)
196
+ data = default.nil? ? v : default.merge(v)
197
+ data.each_pair { |k,v1| add_element(element, k, v1, default) }
198
+ else
199
+ element.value = v
200
+ end
201
+ }
202
+ else
203
+ parent.send("d#{de}=",value)
204
+ end
205
+ end
206
+
207
+ end
208
+
209
+ end
@@ -0,0 +1,110 @@
1
+ require 'edi/mapper'
2
+
3
+ module OpenILS
4
+
5
+ class Mapper < EDI::E::Mapper
6
+ VERSION = '0.8.2'
7
+ end
8
+
9
+ end
10
+
11
+ OpenILS::Mapper.defaults = {
12
+ 'UNB' => { 'S002' => { '0007' => '31B' }, 'S003' => { '0007' => '31B' } },
13
+ 'BGM' => { 'C002' => { '1001' => 220 }, '1225' => 9 },
14
+ 'DTM' => { 'C507' => { '2005' => 137, '2379' => 102 } },
15
+ 'NAD' => { 'C082' => { '3055' => '31B' } },
16
+ 'CUX' => { 'C504' => { '6347' => 2, '6345' => 'USD', '6343' => 9 } },
17
+ 'LIN' => { 'C212' => { '7143' => 'EN' } },
18
+ 'PIA' => { '4347' => 5, 'C212' => { '7143' => 'IB' } },
19
+ 'IMD' => { '7077' => 'F' },
20
+ 'PRI' => { 'C509' => { '5125' => 'AAB' } },
21
+ 'QTY' => { 'C186' => { '6063' => 21 } },
22
+ 'UNS' => { '0081' => 'S' },
23
+ 'CNT' => { 'C270' => { '6069' => 2 } }
24
+ }
25
+
26
+ OpenILS::Mapper.map 'order' do |mapper,key,value|
27
+ mapper.add('BGM', { '1004' => value['po_number'] })
28
+ mapper.add('DTM', { 'C507' => { '2380' => value['date'] } })
29
+ value['buyer'].to_a.each { |buyer| mapper.add('buyer',buyer) }
30
+ value['vendor'].to_a.each { |vendor| mapper.add('vendor',vendor) }
31
+ mapper.add('currency',value['currency'])
32
+ value['items'].each_with_index { |item,index|
33
+ item['line_index'] = index + 1
34
+ item['line_number'] = "#{value['po_number']}/#{index+1}" if item['line_number'].nil?
35
+ mapper.add('item', item)
36
+ }
37
+ mapper.add("UNS", {})
38
+ mapper.add("CNT", { 'C270' => { '6066' => value['line_items'] } })
39
+ end
40
+
41
+ OpenILS::Mapper.map 'item' do |mapper,key,value|
42
+ mapper.add('LIN', { 'C212' => { '7143' => nil }, '1082' => value['line_index'] })
43
+
44
+ # use Array#inject() to group the identifiers in groups of 5.
45
+ # Same as Array#in_groups_of() without the active_support dependency.
46
+ id_groups = value['identifiers'].inject([[]]) { |result,id|
47
+ result.last << id
48
+ if result.last.length == 5
49
+ result << []
50
+ end
51
+ result
52
+ }
53
+
54
+ id_groups.each { |group|
55
+ ids = group.compact.collect { |data|
56
+ id = { '7140' => data['id'] }
57
+ if data['id-qualifier']
58
+ id['7143'] = data['id-qualifier']
59
+ end
60
+ id
61
+ }
62
+ mapper.add('PIA',{ 'C212' => ids })
63
+ }
64
+ value['desc'].each { |desc| mapper.add('desc',desc) }
65
+ mapper.add('QTY', { 'C186' => { '6060' => value['quantity'] } })
66
+ mapper.add('PRI', { 'C509' => { '5118' => value['price'] } })
67
+ mapper.add('RFF', { 'C506' => { '1153' => 'LI', '1154' => value['line_number'] } })
68
+ end
69
+
70
+ OpenILS::Mapper.map('party',/^(buyer|vendor)$/) do |mapper,key,value|
71
+ codes = { 'buyer' => 'BY', 'supplier' => 'SU', 'vendor' => 'SU' }
72
+ party_code = codes[key]
73
+
74
+ if value.is_a?(String)
75
+ value = { 'id' => value }
76
+ end
77
+
78
+ data = {
79
+ '3035' => party_code,
80
+ 'C082' => {
81
+ '3039' => value['id']
82
+ }
83
+ }
84
+ data['C082']['3055'] = value['id-qualifier'] unless value['id-qualifier'].nil?
85
+ mapper.add('NAD', data)
86
+
87
+ if value['reference']
88
+ value['reference'].each_pair { |k,v|
89
+ mapper.add('RFF', { 'C506' => { '1153' => k, '1154' => v }})
90
+ }
91
+ end
92
+ end
93
+
94
+ OpenILS::Mapper.map 'currency' do |mapper,key,value|
95
+ mapper.add('CUX', { 'C504' => ['6345' => value]})
96
+ end
97
+
98
+ OpenILS::Mapper.map 'desc' do |mapper,key,value|
99
+ values = value.to_a.flatten
100
+ while values.length > 0
101
+ code = values.shift
102
+ text = values.shift.to_s
103
+ code_qual = code =~ /^[0-9]+$/ ? 'L' : 'F'
104
+ chunked_text = text.chunk(35)
105
+ while chunked_text.length > 0
106
+ data = [chunked_text.shift,chunked_text.shift].compact
107
+ mapper.add('IMD', { '7077' => code_qual, '7081' => code, 'C273' => { '7008' => data } })
108
+ end
109
+ end
110
+ end
data/test/map_spec.rb ADDED
@@ -0,0 +1,118 @@
1
+ # map_spec.rb
2
+ require 'edi/mapper'
3
+ require 'edi/edi2json'
4
+
5
+ describe EDI::E::Mapper do
6
+
7
+ before(:each) do
8
+ @map = EDI::E::Mapper.new('ORDERS')
9
+ end
10
+
11
+ it "should chunk text" do
12
+ s = 'abcdefghijklmnopqrstuvwxyz'
13
+ s.chunk(5).should == ['abcde','fghij','klmno','pqrst','uvwxy','z']
14
+ end
15
+
16
+ it "should produce an empty purchase order when initialized" do
17
+ ic_text = @map.to_s
18
+ ic_text.should_not be_nil
19
+ ic_text.should_not be_empty
20
+ @map.message.to_s.should == "UNH+1+ORDERS:D:96A:UN'UNT+2+1'"
21
+ end
22
+
23
+ it "should add a single segment in tuple form" do
24
+ @map.add("BGM", {"1225" => 9,"C002" => {"1001" => 220},"1004" => "12345678"})
25
+ @map.message.to_s.should == "UNH+1+ORDERS:D:96A:UN'BGM+220+12345678+9'UNT+3+1'"
26
+ end
27
+
28
+ it "should properly apply defaults" do
29
+ old_defaults = EDI::E::Mapper.defaults
30
+ EDI::E::Mapper.defaults = {
31
+ 'BGM' => { 'C002' => { '1001' => 220 }, '1225' => 9 }
32
+ }
33
+ @map.add("BGM", {"1004" => "12345678"})
34
+ EDI::E::Mapper.defaults = old_defaults
35
+ @map.message.to_s.should == "UNH+1+ORDERS:D:96A:UN'BGM+220+12345678+9'UNT+3+1'"
36
+ end
37
+
38
+ it "should raise an exception if defaults don't look right" do
39
+ lambda {
40
+ EDI::E::Mapper.defaults = 'This is wrong!'
41
+ }.should raise_error(TypeError)
42
+ end
43
+
44
+ it "should properly fill in interchange envelope defaults" do
45
+ map = EDI::E::Mapper.new('ORDERS', nil, {:sender => '123456', :recipient => '654321'})
46
+ map.to_s.should =~ /UNA:\+\.\? 'UNB\+UNOB:3\+123456\+654321\+[0-9]{6}:[0-9]{4}\+1'UNH\+1\+ORDERS:D:96A:UN'UNT\+2\+1'UNZ\+1\+1'/
47
+ end
48
+
49
+ it "should add multiple elements in tuple form" do
50
+ @map.add(
51
+ 'BGM', { 'C002' => { '1001' => 220 }, '1004' => '12345678', '1225' => 9 },
52
+ 'DTM', { 'C507' => { '2005' => 137, '2380' => '20090101', '2379' => 102 }}
53
+ )
54
+ @map.message.to_s.should == "UNH+1+ORDERS:D:96A:UN'BGM+220+12345678+9'DTM+137:20090101:102'UNT+4+1'"
55
+ end
56
+
57
+ it "should add a single element in array form" do
58
+ @map.add(["BGM", {"1225" => 9,"C002" => {"1001" => 220},"1004" => "12345678"}])
59
+ @map.message.to_s.should == "UNH+1+ORDERS:D:96A:UN'BGM+220+12345678+9'UNT+3+1'"
60
+ end
61
+
62
+ it "should add multiple elements in array form" do
63
+ @map.add(
64
+ ['BGM', { 'C002' => { '1001' => 220 }, '1004' => '12345678', '1225' => 9 }],
65
+ ['DTM', { 'C507' => { '2005' => 137, '2380' => '20090101', '2379' => 102 }}]
66
+ )
67
+ @map.message.to_s.should == "UNH+1+ORDERS:D:96A:UN'BGM+220+12345678+9'DTM+137:20090101:102'UNT+4+1'"
68
+ end
69
+
70
+ it "should make use of custom mappings" do
71
+ EDI::E::Mapper.map 'currency' do |mapper,key,value|
72
+ mapper.add('CUX', { 'C504' => [{ '6347' => 2, '6345' => value, '6343' => 9 }]})
73
+ end
74
+
75
+ @map.add(
76
+ 'BGM', { 'C002' => { '1001' => 220 }, '1004' => '12345678', '1225' => 9 },
77
+ 'DTM', { 'C507' => { '2005' => 137, '2380' => '20090101', '2379' => 102 }},
78
+ 'currency', 'USD'
79
+ )
80
+ @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'"
81
+ end
82
+
83
+ it "should raise an exception when an unknown mapping is called" do
84
+ lambda {
85
+ @map.add('everything', { 'answer' => 42 })
86
+ }.should raise_error(NameError)
87
+ end
88
+
89
+ it "should raise an exception when re-registering a named mapping" do
90
+ lambda {
91
+ EDI::E::Mapper.map 'currency' do |mapper,key,value|
92
+ mapper.add('CUX', { 'C504' => [{ '6347' => 2, '6345' => value, '6343' => 9 }]})
93
+ end
94
+ }.should raise_error(NameError)
95
+ end
96
+
97
+ it "should correctly unregister a mapping" do
98
+ EDI::E::Mapper.unregister_mapping 'currency'
99
+
100
+ lambda {
101
+ @map.add(
102
+ 'BGM', { 'C002' => { '1001' => 220 }, '1004' => '12345678', '1225' => 9 },
103
+ 'DTM', { 'C507' => { '2005' => 137, '2380' => '20090101', '2379' => 102 }},
104
+ 'currency', 'USD'
105
+ )
106
+ }.should raise_error(NameError)
107
+ end
108
+
109
+ it "should correctly generate a low-level JEDI hash from an EDIFACT message" do
110
+ interchange = File.open(File.join(File.dirname(__FILE__), 'test_po.edi')) { |f|
111
+ EDI::E::Interchange.parse(f)
112
+ }
113
+ # Can't compare everything because of timestamping, so we'll just compare
114
+ # the bodies for a high degree of confidence
115
+ interchange.to_hash['body'].should == [["ORDERS", {"trailer"=>["UNT", {"0074"=>33, "0062"=>"1"}], "body"=>[["BGM", {"C002"=>{"1001"=>"220"}, "1225"=>"9", "1004"=>"2"}], ["DTM", {"C507"=>{"2005"=>"137", "2380"=>"20090331", "2379"=>"102"}}], ["NAD", {"C082"=>{"3039"=>"3472205", "3055"=>"91"}, "SG2"=>[["RFF", {"C506"=>{"1153"=>"API", "1154"=>"3472205 0001"}}]], "3035"=>"BY"}], ["NAD", {"C082"=>{"3039"=>"3472205", "3055"=>"31B"}, "SG2"=>[["RFF", {"C506"=>{"1153"=>"API", "1154"=>"3472205 0001"}}]], "3035"=>"BY"}], ["NAD", {"C082"=>{"3039"=>"1556150", "3055"=>"31B"}, "3035"=>"SU"}], ["NAD", {"C082"=>{"3039"=>"1556150", "3055"=>"91"}, "SG2"=>[["RFF", {"C506"=>{"1153"=>"IA", "1154"=>"1865"}}]], "3035"=>"SU"}], ["CUX", {"C504"=>{"6345"=>"USD", "6347"=>"2", "6343"=>"9"}}], ["LIN", {"SG25"=>[["PIA", {"C212"=>{"7140"=>"03-0010837", "7143"=>"SA"}, "4347"=>"5"}], ["IMD", {"C273"=>{"7008"=>"Discernment"}, "7077"=>"F", "7081"=>"BTI"}], ["IMD", {"C273"=>{"7008"=>"Concord Records,"}, "7077"=>"F", "7081"=>"BPU"}], ["IMD", {"C273"=>{"7008"=>"1986."}, "7077"=>"F", "7081"=>"BPD"}], ["IMD", {"C273"=>{"7008"=>"1 sound disc :"}, "7077"=>"F", "7081"=>"BPH"}], ["QTY", {"C186"=>{"6060"=>2, "6063"=>"21"}}], ["PRI", {"C509"=>{"5125"=>"AAB", "5118"=>35.95}}], ["RFF", {"C506"=>{"1153"=>"LI", "1154"=>"2/1"}}]], "1082"=>1}], ["LIN", {"SG25"=>[["PIA", {"C212"=>{"7140"=>"03-0010840", "7143"=>"SA"}, "4347"=>"5"}], ["IMD", {"C273"=>{"7008"=>"The inner source"}, "7077"=>"F", "7081"=>"BTI"}], ["IMD", {"C273"=>{"7008"=>"Duke, George, 1946-"}, "7077"=>"F", "7081"=>"BAU"}], ["IMD", {"C273"=>{"7008"=>"MPS Records,"}, "7077"=>"F", "7081"=>"BPU"}], ["IMD", {"C273"=>{"7008"=>"1973."}, "7077"=>"F", "7081"=>"BPD"}], ["IMD", {"C273"=>{"7008"=>"2 sound discs :"}, "7077"=>"F", "7081"=>"BPH"}], ["QTY", {"C186"=>{"6060"=>1, "6063"=>"21"}}], ["PRI", {"C509"=>{"5125"=>"AAB", "5118"=>28.95}}], ["RFF", {"C506"=>{"1153"=>"LI", "1154"=>"2/2"}}]], "1082"=>2}], ["UNS", {"0081"=>"S"}], ["CNT", {"C270"=>{"6069"=>"2", "6066"=>2}}], ["UNT", {"0074"=>33, "0062"=>"1"}]], "header"=>["UNH", {"S009"=>{"0052"=>"D", "0065"=>"ORDERS", "0054"=>"96A", "0051"=>"UN"}, "0062"=>"1"}]}]]
116
+ end
117
+
118
+ end
@@ -0,0 +1,41 @@
1
+ require 'openils/mapper'
2
+
3
+ describe OpenILS::Mapper do
4
+
5
+ before(:each) do
6
+ @map = OpenILS::Mapper.new('ORDERS')
7
+ end
8
+
9
+ it "should add both qualified and unqualified buyer/vendor fields" do
10
+ @map.add(
11
+ ['buyer', { 'id' => '3472205', 'id-qualifier' => '91', 'reference' => { 'API' => '3472205 0001' } }],
12
+ ['buyer', { 'id' => '3472205', 'reference' => { 'API' => '3472205 0001' }}]
13
+ )
14
+ @map.add(
15
+ 'vendor', '1556150',
16
+ 'vendor', { 'id' => '1556150', 'id-qualifier' => '91', 'reference' => { 'IA' => '1865' }}
17
+ )
18
+ @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'"
19
+ end
20
+
21
+ it "should properly chunk and add descriptive fields" do
22
+ @map.add(
23
+ 'desc', [
24
+ 'BAU', 'Campbell, James',
25
+ 'BTI', "The Ghost Mountain boys : their epic march and the terrifying battle for New Guinea -- the forgotten war of the South Pacific",
26
+ 'BPU', 'Crown Publishers',
27
+ 'BPD', 2007
28
+ ]
29
+ )
30
+ @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'"
31
+ end
32
+
33
+ it "should create a message from high-level JEDI input" do
34
+ json = File.read(File.join(File.dirname(__FILE__), 'test_po.json'))
35
+ @map = OpenILS::Mapper.from_json(%{{ "msg_type": "ORDERS", "msg": #{json}, "sender": "123456", "recipient": {"id": "999999999", "id-qualifier": "1"}}})
36
+ @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'"
37
+ @map.header.cS002.to_s.should == "123456:31B"
38
+ @map.header.cS003.to_s.should == "999999999:1"
39
+ end
40
+
41
+ end
data/test/test_po.edi ADDED
@@ -0,0 +1,36 @@
1
+ UNA:+.? '
2
+ UNB+UNOB:3+++090401:1152+1'
3
+ UNH+1+ORDERS:D:96A:UN'
4
+ BGM+220+2+9'
5
+ DTM+137:20090331:102'
6
+ NAD+BY+3472205::91'
7
+ RFF+API:3472205 0001'
8
+ NAD+BY+3472205::31B'
9
+ RFF+API:3472205 0001'
10
+ NAD+SU+1556150::31B'
11
+ NAD+SU+1556150::91'
12
+ RFF+IA:1865'
13
+ CUX+2:USD:9'
14
+ LIN+1'
15
+ PIA+5+03-0010837:SA'
16
+ IMD+F+BTI+:::Discernment'
17
+ IMD+F+BPU+:::Concord Records,'
18
+ IMD+F+BPD+:::1986.'
19
+ IMD+F+BPH+:::1 sound disc ?:'
20
+ QTY+21:2'
21
+ PRI+AAB:35.95'
22
+ RFF+LI:2/1'
23
+ LIN+2'
24
+ PIA+5+03-0010840:SA'
25
+ IMD+F+BTI+:::The inner source'
26
+ IMD+F+BAU+:::Duke, George, 1946-'
27
+ IMD+F+BPU+:::MPS Records,'
28
+ IMD+F+BPD+:::1973.'
29
+ IMD+F+BPH+:::2 sound discs ?:'
30
+ QTY+21:1'
31
+ PRI+AAB:28.95'
32
+ RFF+LI:2/2'
33
+ UNS+S'
34
+ CNT+2:2'
35
+ UNT+33+1'
36
+ UNZ+1+1'
data/test/test_po.json ADDED
@@ -0,0 +1,36 @@
1
+ ["order", {
2
+ "po_number":2,
3
+ "date":"20090331",
4
+ "buyer":[
5
+ {"id-qualifier":"91","id":"3472205","reference":{"API":"3472205 0001"}},
6
+ {"id":"3472205","reference":{"API":"3472205 0001"}}
7
+ ],
8
+ "vendor":[
9
+ "1556150",
10
+ {"id-qualifier":"91","reference":{"IA":"1865"},"id":"1556150"}
11
+ ],
12
+ "currency":"USD",
13
+ "items":[{
14
+ "identifiers":[{"id-qualifier":"SA","id":"03-0010837"}],
15
+ "price":35.95,
16
+ "desc":[
17
+ {"BTI":"Discernment"},
18
+ {"BPU":"Concord Records,"},
19
+ {"BPD":"1986."},
20
+ {"BPH":"1 sound disc :"}
21
+ ],
22
+ "quantity":2
23
+ },{
24
+ "identifiers":[{"id-qualifier":"SA","id":"03-0010840"}],
25
+ "price":28.95,
26
+ "desc":[
27
+ {"BTI":"The inner source"},
28
+ {"BAU":"Duke, George, 1946-"},
29
+ {"BPU":"MPS Records,"},
30
+ {"BPD":"1973."},
31
+ {"BPH":"2 sound discs :"}
32
+ ],
33
+ "quantity":1
34
+ }],
35
+ "line_items":2
36
+ }]
metadata ADDED
@@ -0,0 +1,123 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: openils-mapper
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.8.2
5
+ platform: ruby
6
+ authors:
7
+ - Michael B. Klein
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2010-03-01 00:00:00 -08:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: edi4r
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: 0.9.4
24
+ version:
25
+ - !ruby/object:Gem::Dependency
26
+ name: edi4r-tdid
27
+ type: :runtime
28
+ version_requirement:
29
+ version_requirements: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 0.6.5
34
+ version:
35
+ - !ruby/object:Gem::Dependency
36
+ name: json
37
+ type: :runtime
38
+ version_requirement:
39
+ version_requirements: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: 1.1.3
44
+ version:
45
+ - !ruby/object:Gem::Dependency
46
+ name: rcov
47
+ type: :development
48
+ version_requirement:
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: 0.8.1
54
+ version:
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ type: :development
58
+ version_requirement:
59
+ version_requirements: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ version: 1.2.2
64
+ version:
65
+ - !ruby/object:Gem::Dependency
66
+ name: rake
67
+ type: :development
68
+ version_requirement:
69
+ version_requirements: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ version: 0.8.0
74
+ version:
75
+ description: Middleware layer to provide translation between high-level JSON and raw EDIFACT messages
76
+ email: mbklein@gmail.com
77
+ executables: []
78
+
79
+ extensions: []
80
+
81
+ extra_rdoc_files:
82
+ - README.rdoc
83
+ files:
84
+ - Rakefile
85
+ - README.rdoc
86
+ - lib/edi/edi2json.rb
87
+ - lib/edi/mapper.rb
88
+ - lib/openils/mapper.rb
89
+ - test/map_spec.rb
90
+ - test/openils_map_spec.rb
91
+ - test/test_po.edi
92
+ - test/test_po.json
93
+ has_rdoc: true
94
+ homepage:
95
+ licenses: []
96
+
97
+ post_install_message:
98
+ rdoc_options:
99
+ - --main
100
+ - README.rdoc
101
+ require_paths:
102
+ - lib
103
+ required_ruby_version: !ruby/object:Gem::Requirement
104
+ requirements:
105
+ - - ">="
106
+ - !ruby/object:Gem::Version
107
+ version: "0"
108
+ version:
109
+ required_rubygems_version: !ruby/object:Gem::Requirement
110
+ requirements:
111
+ - - ">="
112
+ - !ruby/object:Gem::Version
113
+ version: "0"
114
+ version:
115
+ requirements: []
116
+
117
+ rubyforge_project:
118
+ rubygems_version: 1.3.5
119
+ signing_key:
120
+ specification_version: 3
121
+ summary: EDIFACT<->JSON middleware for the Evergreen Open Source ILS
122
+ test_files: []
123
+