github.com/moov-io/imagecashletter@v0.10.1/writer.go (about) 1 // Copyright 2020 The Moov Authors 2 // Use of this source code is governed by an Apache License 3 // license that can be found in the LICENSE file. 4 5 package imagecashletter 6 7 import ( 8 "bufio" 9 "encoding/binary" 10 "fmt" 11 "io" 12 13 "github.com/gdamore/encoding" 14 ) 15 16 // Writer writes an ImageCashLetter/X9 File to an encoded format. 17 // 18 // Callers should use NewWriter to create a new instance and apply WriterOptions 19 // as needed to properly encode files for their usecase. 20 type Writer struct { 21 w *bufio.Writer 22 lineNum int // current line being written 23 VariableLineLength bool 24 EbcdicEncoding bool 25 } 26 27 // NewWriter returns a new Writer that writes to w. 28 func NewWriter(w io.Writer, opts ...WriterOption) *Writer { 29 writer := &Writer{ 30 w: bufio.NewWriter(w), 31 } 32 for _, opt := range opts { 33 opt(writer) 34 } 35 return writer 36 } 37 38 // WriterOption allows Writer to be configured to write in different formats 39 type WriterOption func(w *Writer) 40 41 // WriteVariableLineLengthOption allows Writer to write control bytes ahead of record to describe how long the line is 42 // Follows DSTU microformat as defined https://www.frbservices.org/assets/financial-services/check/setup/frb-x937-standards-reference.pdf 43 func WriteVariableLineLengthOption() WriterOption { 44 return func(w *Writer) { 45 w.VariableLineLength = true 46 } 47 } 48 49 // WriteEbcdicEncodingOption allows Writer to write file in EBCDIC 50 // Follows DSTU microformat as defined https://www.frbservices.org/assets/financial-services/check/setup/frb-x937-standards-reference.pdf 51 func WriteEbcdicEncodingOption() WriterOption { 52 return func(w *Writer) { 53 w.EbcdicEncoding = true 54 } 55 } 56 57 func (w *Writer) writeLine(record FileRecord) error { 58 line := record.String() 59 lineLength := len(line) 60 61 if w.VariableLineLength { 62 ctrl := make([]byte, 4) 63 binary.BigEndian.PutUint32(ctrl, uint32(lineLength)) 64 if _, err := w.w.Write(ctrl); err != nil { 65 return err 66 } 67 } 68 69 if w.EbcdicEncoding { 70 if ivData, ok := record.(*ImageViewData); ok { 71 // need to encode everything other than binary image into EBCDIC 72 encoded, err := encoding.EBCDIC.NewEncoder().String(ivData.toString(false)) 73 if err != nil { 74 return err 75 } 76 if _, err := w.w.WriteString(encoded); err != nil { 77 return err 78 } 79 if _, err := w.w.Write(ivData.ImageData); err != nil { 80 return err 81 } 82 } else { 83 // no binary data in record, encode entire line 84 encoded, err := encoding.EBCDIC.NewEncoder().String(line) 85 if err != nil { 86 return err 87 } 88 if _, err := w.w.WriteString(encoded); err != nil { 89 return err 90 } 91 } 92 } else { 93 // ASCII encoding by default 94 if _, err := w.w.WriteString(line); err != nil { 95 return err 96 } 97 } 98 99 if !w.VariableLineLength { 100 if _, err := w.w.WriteString("\n"); err != nil { 101 return err 102 } 103 } 104 105 w.lineNum++ 106 return nil 107 } 108 109 // Writer writes a single imagecashletter.file record to w 110 func (w *Writer) Write(file *File) error { 111 if file == nil { 112 return ErrNilFile 113 } 114 if err := file.Validate(); err != nil { 115 return err 116 } 117 w.lineNum = 0 118 // Iterate over all records in the file 119 if err := w.writeLine(&file.Header); err != nil { 120 return err 121 } 122 if err := w.writeCashLetter(file); err != nil { 123 return err 124 } 125 if err := w.writeLine(&file.Control); err != nil { 126 return err 127 } 128 129 return w.w.Flush() 130 } 131 132 // Flush writes any buffered data to the underlying io.Writer. 133 // To check if an error occurred during the Flush, call Error. 134 func (w *Writer) Flush() { 135 w.w.Flush() 136 } 137 138 // writeCashLetter writes a CashLetter to a file 139 func (w *Writer) writeCashLetter(file *File) error { 140 for _, cl := range file.CashLetters { 141 if err := w.writeLine(cl.GetHeader()); err != nil { 142 return err 143 } 144 for _, ci := range cl.GetCreditItems() { 145 if err := w.writeLine(ci); err != nil { 146 return err 147 } 148 } 149 for _, credit := range cl.GetCredits() { 150 if err := w.writeLine(credit); err != nil { 151 return err 152 } 153 } 154 if err := w.writeBundle(cl); err != nil { 155 return err 156 } 157 for _, rns := range cl.GetRoutingNumberSummary() { 158 if err := w.writeLine(rns); err != nil { 159 return err 160 } 161 } 162 if err := w.writeLine(cl.GetControl()); err != nil { 163 return err 164 } 165 } 166 return nil 167 } 168 169 // writeBundle writes a Bundle to a CashLetter 170 func (w *Writer) writeBundle(cl CashLetter) error { 171 for _, b := range cl.GetBundles() { 172 if err := w.writeLine(b.GetHeader()); err != nil { 173 return err 174 } 175 if len(b.Checks) > 0 { 176 if err := w.writeCheckDetail(b); err != nil { 177 return err 178 } 179 } 180 if len(b.Returns) > 0 { 181 if err := w.writeReturnDetail(b); err != nil { 182 return err 183 } 184 } 185 if err := w.writeLine(b.GetControl()); err != nil { 186 return err 187 } 188 } 189 return nil 190 } 191 192 // writeCheckDetail writes a CheckDetail to a Bundle 193 func (w *Writer) writeCheckDetail(b *Bundle) error { 194 for _, cd := range b.GetChecks() { 195 if err := w.writeLine(cd); err != nil { 196 return err 197 } 198 // Write CheckDetailsAddendum (A, B, C) 199 if err := w.writeCheckDetailAddendum(cd); err != nil { 200 return err 201 } 202 if err := w.writeCheckImageView(cd); err != nil { 203 return err 204 } 205 } 206 return nil 207 } 208 209 // writeCheckDetailAddendum writes a CheckDetailAddendum (A, B, C) to a CheckDetail 210 func (w *Writer) writeCheckDetailAddendum(cd *CheckDetail) error { 211 addendumA := cd.GetCheckDetailAddendumA() 212 for i := range addendumA { 213 if err := w.writeLine(&addendumA[i]); err != nil { 214 return err 215 } 216 } 217 218 addendumB := cd.GetCheckDetailAddendumB() 219 for i := range addendumB { 220 if err := w.writeLine(&addendumB[i]); err != nil { 221 return err 222 } 223 } 224 225 addendumC := cd.GetCheckDetailAddendumC() 226 for i := range addendumC { 227 if err := w.writeLine(&addendumC[i]); err != nil { 228 return err 229 } 230 } 231 return nil 232 } 233 234 // writeCheckImageView writes ImageViews (Detail, Data, Analysis) to a CheckDetail 235 func (w *Writer) writeCheckImageView(cd *CheckDetail) error { 236 237 ivDetailSlice := cd.GetImageViewDetail() 238 ivDataSlice := cd.GetImageViewData() 239 ivAnalysisSlice := cd.GetImageViewAnalysis() 240 241 // TODO: Add validator to ensure that each imageViewDetail has a corresponding imageViewData and imageViewAnalysis 242 // for now enforce that all images have data and analysis or no images have data and analysis 243 244 if len(ivDataSlice) > 0 && len(ivDataSlice) != len(ivDetailSlice) { 245 // should be same number of imageViewData as imageViewDetail 246 msg := fmt.Sprintf(msgBundleImageDetailCount, len(ivDataSlice)) 247 return &BundleError{FieldName: "ImageViewData", Msg: msg} 248 } 249 250 if len(ivAnalysisSlice) > 0 && len(ivAnalysisSlice) != len(ivDetailSlice) { 251 // should same number of imageViewAnalysis and imageViewDetail 252 msg := fmt.Sprintf(msgBundleImageDetailCount, len(ivAnalysisSlice)) 253 return &BundleError{FieldName: "ImageViewAnalysis", Msg: msg} 254 } 255 256 // FRB asks that imageViewDetail should immediately be followed by its corresponding data and analysis 257 for i := range ivDetailSlice { 258 if err := w.writeLine(&ivDetailSlice[i]); err != nil { 259 return err 260 } 261 if len(ivDataSlice) > 0 && len(ivDataSlice) >= i-1 { 262 ivData := ivDataSlice[i] 263 if err := w.writeLine(&ivData); err != nil { 264 return err 265 } 266 } 267 if len(ivAnalysisSlice) > 0 && len(ivAnalysisSlice) >= i-1 { 268 ivAnalysis := ivAnalysisSlice[i] 269 if err := w.writeLine(&ivAnalysis); err != nil { 270 return err 271 } 272 } 273 } 274 275 return nil 276 } 277 278 // writeReturnDetail writes a ReturnDetail to a ReturnBundle 279 func (w *Writer) writeReturnDetail(b *Bundle) error { 280 for _, rd := range b.GetReturns() { 281 if err := w.writeLine(rd); err != nil { 282 return err 283 } 284 // Write ReturnDetailAddendum (A, B, C, D) 285 if err := w.writeReturnDetailAddendum(rd); err != nil { 286 return err 287 } 288 if err := w.writeReturnImageView(rd); err != nil { 289 return err 290 } 291 } 292 return nil 293 } 294 295 // writeReturnDetailAddendum writes a ReturnDetailAddendum (A, B, C, D) to a ReturnDetail 296 func (w *Writer) writeReturnDetailAddendum(rd *ReturnDetail) error { 297 addendumA := rd.GetReturnDetailAddendumA() 298 for i := range addendumA { 299 if err := w.writeLine(&addendumA[i]); err != nil { 300 return err 301 } 302 } 303 304 addendumB := rd.GetReturnDetailAddendumB() 305 for i := range addendumB { 306 if err := w.writeLine(&addendumB[i]); err != nil { 307 return err 308 } 309 } 310 311 addendumC := rd.GetReturnDetailAddendumC() 312 for i := range addendumC { 313 if err := w.writeLine(&addendumC[i]); err != nil { 314 return err 315 } 316 } 317 318 addendumD := rd.GetReturnDetailAddendumD() 319 for i := range addendumD { 320 if err := w.writeLine(&addendumD[i]); err != nil { 321 return err 322 } 323 } 324 return nil 325 } 326 327 // writeReturnImageView writes ImageViews (Detail, Data, Analysis) to a ReturnDetail 328 func (w *Writer) writeReturnImageView(rd *ReturnDetail) error { 329 ivDetail := rd.GetImageViewDetail() 330 for i := range ivDetail { 331 if err := w.writeLine(&ivDetail[i]); err != nil { 332 return err 333 } 334 } 335 336 ivData := rd.GetImageViewData() 337 for i := range ivData { 338 if err := w.writeLine(&ivData[i]); err != nil { 339 return err 340 } 341 } 342 343 ivAnalysis := rd.GetImageViewAnalysis() 344 for i := range ivAnalysis { 345 if err := w.writeLine(&ivAnalysis[i]); err != nil { 346 return err 347 } 348 } 349 return nil 350 }