Skip to content

Commit c368009

Browse files
author
Viacheslav Dobromyslov
committed
Added XSD to docs.
Added methods to add parser listeners. Implemented XML conversion to typed objects. Simplified parser API. Implemented async promisable parser.
1 parent bf1385d commit c368009

File tree

10 files changed

+2624
-129
lines changed

10 files changed

+2624
-129
lines changed

README.md

Lines changed: 8 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -37,31 +37,22 @@ Here is a common usage example:
3737
import {CommerceMlImportParser} from 'commerceml-parser/import-parser';
3838
import {createReadStream} from "fs";
3939

40-
// Create parser stream for CommerceML catalog import file
41-
const parserStream = new CommerceMlImportParser().createStream();
40+
// Create parser for CommerceML catalog import file
41+
const catalogImportParser = new CommerceMlImportParser();
4242

4343
// Define handler for classifier XML block
44-
parserStream.on('classifier', data => {
45-
console.log('classifier:', JSON.stringify(data));
44+
catalogImportParser.onClassifier(classifier => {
45+
console.log('classifier', JSON.stringify(classifier));
4646
});
4747

48-
// Define handler for classifier group XML blocks
49-
parserStream.on('classifierGroup', data => {
50-
console.log('classifierGroup:', JSON.stringify(data));
51-
});
5248

53-
// Define handler for parser error
54-
parserStream.on('error', error => {
55-
console.log('error', error);
56-
});
57-
58-
// Define handler for file end
59-
parserStream.on('end', _ => {
60-
console.log('Done.');
49+
// Define handler for classifier group XML blocks
50+
catalogImportParser.onClassifierGroup(classifierGroup => {
51+
console.log('classifierGroup', JSON.stringify(classifierGroup));
6152
});
6253

6354
// Read CommerceML file and feed it to the parser stream
64-
createReadStream('./data/import0_1_with_nested_groups.xml').pipe(parserStream);
55+
await catalogImportParser.parse(createReadStream('./data/import0_1_with_nested_groups.xml'));
6556
```
6657

6758
## Thanks to

doc/commerceml-2.08.pdf

515 KB
Binary file not shown.

doc/commerceml-2.08.xsd

Lines changed: 2016 additions & 0 deletions
Large diffs are not rendered by default.

spec/example.spec.ts

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,16 @@ import {createReadStream} from "fs";
33

44
describe('CommerceMlParser', () => {
55
it('is ok', async () => {
6-
const stream = new CommerceMlImportParser().createStream();
7-
stream.on('classifier', data => {
8-
console.log('classifier:', JSON.stringify(data));
6+
const catalogImportParser = new CommerceMlImportParser();
7+
catalogImportParser.onClassifier(classifier => {
8+
console.log('classifier', JSON.stringify(classifier));
99
});
1010

11-
stream.on('classifierGroup', data => {
12-
console.log('classifierGroup:', JSON.stringify(data));
11+
catalogImportParser.onClassifierGroup(classifierGroup => {
12+
console.log('classifierGroup', JSON.stringify(classifierGroup));
1313
});
1414

15-
stream.on('error', error => {
16-
console.log('error', error);
17-
});
18-
19-
createReadStream('./data/import0_1_with_nested_groups.xml').pipe(stream);
15+
await catalogImportParser.parse(createReadStream('./data/import0_1_with_nested_groups.xml'));
16+
console.log('Done');
2017
});
2118
});

src/abstract-parser.ts

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {createStream, QualifiedTag, SAXStream, Tag} from 'sax';
22
import {encodeXML} from 'entities/lib';
33
import X2JS from 'x2js';
4+
import {Readable} from 'stream';
45

56
export class CommerceMlCollectRule {
67
start: string[] = [];
@@ -11,7 +12,7 @@ export abstract class CommerceMlAbstractParser {
1112
/**
1213
* SAX Parser instance.
1314
*/
14-
protected parser?: SAXStream;
15+
protected parser: SAXStream;
1516

1617
/**
1718
*
@@ -43,10 +44,7 @@ export abstract class CommerceMlAbstractParser {
4344
*/
4445
protected collectOpenTags: string[] = [];
4546

46-
/**
47-
* Creates SAX stream for XML parsing.
48-
*/
49-
public createStream(): SAXStream {
47+
constructor() {
5048
this.parser = createStream(true, {
5149
trim: true,
5250
normalize: true
@@ -63,7 +61,30 @@ export abstract class CommerceMlAbstractParser {
6361
this.parser.on('text', (text: string) => {
6462
this.onText(text);
6563
});
64+
}
65+
66+
/**
67+
* Starts parsing readable stream.
68+
* @param readStream
69+
*/
70+
public async parse(readStream: Readable): Promise<void> {
71+
return new Promise((resolve, reject) => {
72+
this.parser.on('end', () => {
73+
resolve();
74+
});
75+
76+
this.parser.on('error', error => {
77+
reject(error);
78+
});
79+
80+
readStream.pipe(this.parser);
81+
});
82+
}
6683

84+
/**
85+
* Returns SAX stream.
86+
*/
87+
public getStream(): SAXStream {
6788
return this.parser;
6889
}
6990

src/import-parser.ts

Lines changed: 237 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,245 @@
11
import {CommerceMlAbstractParser, CommerceMlCollectRule} from './abstract-parser';
2+
import {
3+
Catalog,
4+
Classifier,
5+
ClassifierGroup,
6+
ClassifierProperty,
7+
CommercialInformation,
8+
Counterparty, DictionaryValue, Product,
9+
PropertyValue, RequisiteValue
10+
} from './types';
211

312
export class CommerceMlImportParser extends CommerceMlAbstractParser {
13+
/**
14+
* Parses commercial information schemaVersion and creationTimestamp attributes.
15+
* @param callback
16+
*/
17+
public onCommercialInformation(callback: (commercialInformation: CommercialInformation) => void): void {
18+
this.parser.on('commercialInformation', (data: any) => {
19+
const commercialInformation: CommercialInformation = {
20+
schemaVersion: data.КоммерческаяИнформация._ВерсияСхемы,
21+
creationTimestamp: data.КоммерческаяИнформация._ДатаФормирования
22+
};
23+
24+
callback(commercialInformation);
25+
});
26+
}
27+
28+
/**
29+
* Parses classifier block header without details.
30+
* @param callback
31+
*/
32+
public onClassifier(callback: (classifier: Classifier) => void): void {
33+
this.parser.on('classifier', (data: any) => {
34+
const classifierXml = data.Классификатор;
35+
const classifier: Classifier = {
36+
id: classifierXml.Ид,
37+
name: classifierXml.Наименование,
38+
owner: this.parseCounterpartyXmlData(classifierXml.Владелец)
39+
};
40+
41+
callback(classifier);
42+
});
43+
}
44+
45+
/**
46+
* Parses classifier groups.
47+
* @param callback
48+
*/
49+
public onClassifierGroup(callback: (classifierGroup: ClassifierGroup) => void): void {
50+
const processGroup = (groupData: any): ClassifierGroup => {
51+
const result: ClassifierGroup = {
52+
id: groupData.Ид,
53+
name: groupData.Наименование
54+
};
55+
56+
const children: ClassifierGroup[] = [];
57+
if (groupData.Группы?.Группа?.length > 0) {
58+
for (const child of groupData.Группы.Группа) {
59+
children.push(processGroup(child));
60+
}
61+
}
62+
63+
if (children.length > 0) {
64+
result.groups = children;
65+
}
66+
67+
return result;
68+
};
69+
70+
this.parser.on('classifierGroup', (data: any) => {
71+
const classifierGroupXml = data.Группа;
72+
const classifierGroup: ClassifierGroup = processGroup(classifierGroupXml);
73+
callback(classifierGroup);
74+
});
75+
}
76+
77+
/**
78+
* Parses classifier properties.
79+
* @param callback
80+
*/
81+
public onClassifierProperty(callback: (classifierProperty: ClassifierProperty) => void): void {
82+
this.parser.on('classifierProperty', (data: any) => {
83+
const propertyXml = data.Свойство;
84+
85+
const classifierProperty: ClassifierProperty = {
86+
id: propertyXml.Ид,
87+
name: propertyXml.Наименование,
88+
type: propertyXml.ТипЗначений
89+
};
90+
91+
if (propertyXml.ВариантыЗначений?.Справочник?.length > 0) {
92+
classifierProperty.dictionaryValues = [];
93+
for (const dictionaryValue of propertyXml.ВариантыЗначений.Справочник) {
94+
classifierProperty.dictionaryValues.push({
95+
id: dictionaryValue.ИдЗначения,
96+
value: dictionaryValue.Значение
97+
} as DictionaryValue);
98+
}
99+
}
100+
101+
callback(classifierProperty);
102+
});
103+
}
104+
105+
/**
106+
* Parses catalog header without details.
107+
* @param callback
108+
*/
109+
public onCatalog(callback: (catalog: Catalog) => void) {
110+
this.parser.on('catalog', (data: any) => {
111+
const catalogXml = data.Каталог;
112+
const catalog: Catalog = {
113+
id: catalogXml.Ид,
114+
classifierId: catalogXml.ИдКлассификатора,
115+
name: catalogXml.Наименование,
116+
owner: this.parseCounterpartyXmlData(catalogXml.Владелец),
117+
products: []
118+
};
119+
120+
callback(catalog);
121+
});
122+
}
123+
124+
/**
125+
* Parses catalog products.
126+
* @param callback
127+
*/
128+
public onProduct(callback: (product: Product) => void) {
129+
this.parser.on('product', (data: any) => {
130+
const productXml = data.Товар;
131+
const product: Product = {
132+
id: productXml.Ид,
133+
article: productXml.Артикул,
134+
name: productXml.Наименование,
135+
baseMeasurementUnit: {
136+
code: productXml.БазоваяЕдиница._Код,
137+
fullName: productXml.БазоваяЕдиница._НаименованиеПолное,
138+
acronym: productXml.БазоваяЕдиница._МеждународноеСокращение
139+
},
140+
group: productXml.Группы.Ид,
141+
description: productXml.Описание
142+
};
143+
144+
if (productXml.СтавкиНалогов?.СтавкаНалога) {
145+
if (Array.isArray(productXml.СтавкиНалогов?.СтавкаНалога)) {
146+
product.taxRates = [];
147+
for (const taxRateXml of productXml.СтавкиНалогов?.СтавкаНалога) {
148+
product.taxRates.push({
149+
name: taxRateXml.Наименование,
150+
rate: taxRateXml.Ставка
151+
});
152+
}
153+
} else {
154+
product.taxRates = [{
155+
name: productXml.СтавкиНалогов.СтавкаНалога.Наименование,
156+
rate: productXml.СтавкиНалогов.СтавкаНалога.Ставка
157+
}];
158+
}
159+
}
160+
161+
if (productXml.Штрихкод) {
162+
product.barcode = productXml.Штрихкод;
163+
}
164+
165+
if (productXml.Изготовитель) {
166+
product.manufacturer = {
167+
id: productXml.Изготовитель.Ид,
168+
name: productXml.Изготовитель.Наименование
169+
};
170+
}
171+
172+
if (productXml.ЗначенияСвойств?.ЗначенияСвойства?.length > 0) {
173+
product.propertyValues = [];
174+
for (const propertyValue of productXml.ЗначенияСвойств.ЗначенияСвойства) {
175+
if (Array.isArray(propertyValue.Значение)) {
176+
product.propertyValues.push({
177+
id: propertyValue.Ид,
178+
values: propertyValue.Значение
179+
} as PropertyValue);
180+
} else {
181+
product.propertyValues.push({
182+
id: propertyValue.Ид,
183+
values: [propertyValue.Значение]
184+
} as PropertyValue);
185+
}
186+
}
187+
}
188+
189+
if (productXml.ЗначенияРеквизитов?.ЗначениеРеквизита?.length > 0) {
190+
product.requisiteValues = [];
191+
for (const requisiteValue of productXml.ЗначенияРеквизитов.ЗначениеРеквизита ?? []) {
192+
if (Array.isArray(requisiteValue.Значение)) {
193+
product.requisiteValues.push({
194+
name: requisiteValue.Наименование,
195+
values: requisiteValue.Значение
196+
} as RequisiteValue);
197+
} else {
198+
product.requisiteValues.push({
199+
name: requisiteValue.Наименование,
200+
values: [requisiteValue.Значение]
201+
} as RequisiteValue);
202+
}
203+
}
204+
}
205+
206+
callback(product);
207+
});
208+
}
209+
210+
/**
211+
* Helper method to parse counterparty XML data.
212+
* @param xmlData
213+
*/
214+
protected parseCounterpartyXmlData(xmlData: any): Counterparty {
215+
const counterparty: Counterparty = {
216+
id: xmlData.Ид,
217+
name: xmlData.Наименование
218+
};
219+
220+
// Detect company info or person info
221+
if (xmlData.ОфициальноеНаименование) {
222+
counterparty.companyInfo = {
223+
officialName: xmlData.ОфициальноеНаименование,
224+
inn: xmlData.ИНН,
225+
kpp: xmlData.КПП,
226+
okpo: xmlData.ОКПО
227+
};
228+
} else {
229+
counterparty.personInfo = {
230+
fullName: xmlData.ПолноеНаименование
231+
};
232+
}
233+
234+
return counterparty;
235+
}
236+
237+
/**
238+
* Parser rules.
239+
*/
4240
protected getCollectRules(): {[key: string]: CommerceMlCollectRule} {
5241
return {
6-
commercialInfo: {
242+
commercialInformation: {
7243
start: ['КоммерческаяИнформация']
8244
},
9245
classifier: {

0 commit comments

Comments
 (0)