forked from reaster/saxy
-
Notifications
You must be signed in to change notification settings - Fork 0
/
OXTutorialTests.m
374 lines (281 loc) · 20 KB
/
OXTutorialTests.m
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
/**
OXTutorialTests.m
SAXy OX - Object-to-XML mapping library
Hands-on SAXy tutorial. Topics covered:
1) A simple XML mapping example
2) Library anatomy - OXmlReader, OXmlWriter, OXmlMapper and OXmlElementMapper
3) Concise mapping declarations - the builder pattern
4) Reading lists of data
5) Working with attributes and body text
6) OXmlXPathMapper - for fine-grained control
7) Namespace support
Created by Richard Easterling on 2/26/13.
*/
#import <SenTestingKit/SenTestingKit.h>
#import "OXmlReader.h"
#import "OXmlWriter.h"
#import "OXmlMapper.h"
#import "OXmlElementMapper.h"
#import "OXmlXPathMapper.h"
////////////////////////////////////////////////////////////////////////////////////////
#pragma mark - test class
////////////////////////////////////////////////////////////////////////////////////////
@interface CartoonCharacter : NSObject
@property(nonatomic)NSString *firstName;
@property(nonatomic)NSString *lastName;
@property(nonatomic)NSDate *birthDay;
@end
@implementation CartoonCharacter
@end
////////////////////////////////////////////////////////////////////////////////////////
#pragma mark - tutorial
////////////////////////////////////////////////////////////////////////////////////////
@interface OXTutorialTests : SenTestCase @end
@implementation OXTutorialTests
/**
SAXy makes working with XML as easy as possible. Here is a very simple example demonstrating
reading and writing XML using the CartoonCharacter class.
*/
- (void)testXMLMappingMadeSimple
{
NSString *xml = @"<tune><firstName>Daffy</firstName><lastName>Duck</lastName></tune>";
//map 'tune' element to CartoonCharacter class:
OXmlMapper *mapper = [[OXmlMapper mapper] elements:@[ [OXmlElementMapper rootXPath:@"/tune" type:[CartoonCharacter class]] ]];
OXmlReader *reader = [OXmlReader readerWithMapper:mapper]; //creates a reader based on the mapper
CartoonCharacter *duck = [reader readXmlText:xml]; //reads xml
STAssertEqualObjects(@"Daffy", duck.firstName, @"mapped 'firstName' element to 'firstName' property"); //test results
STAssertEqualObjects(@"Duck", duck.lastName, @"mapped 'lastName' element to 'lastName' property");
OXmlWriter *writer = [OXmlWriter writerWithMapper:mapper]; //creates a writer based on mapper
writer.xmlHeader = nil; //doesn't include XML header so we can campare result string
NSString *output = [writer writeXml:duck prettyPrint:NO]; //writes xml
STAssertEqualObjects(xml, output, @"input xml equals output xml");
}
/**
SAXy's most important XML mapping classes are:
1) OXmlReader - used for reading XML (unmarshalling) to class instances
2) OXmlWriter - used for writing XML (marshalling) from class instances
3) OXmlMapper - high-level mapping of how to convert an XML schema to an object hierarchy
4) OXmlElementMapper - describes how attributes and elements are converted to class properties
Readers and writers are easy to use and understand, all they require to do their job is a mapper instance.
Mappers are basically a list of element mappings, starting with a root mapping.
Element mappers associate XML element and attribute names with class properties. For specifying xml, SAXy
supports a subset of the xpath language, but most of the time you can just think element names when you see xpath.
This example modifies the last by explicitly mapping the CartoonCharacter's properties, something that was done
automatically by SAXy before. We'll also shorten the element names to make it more interesting.
*/
- (void)testLibraryAnatomy
{
NSString *xml = @"<tune><first>Daffy</first><last>Duck</last></tune>";
//map the '/tune' path to the CartoonCharacter class:
OXmlElementMapper *rootMapper = [OXmlElementMapper rootXPath:@"/tune" type:[CartoonCharacter class]];
//map CartoonCharacter properties:
OXmlElementMapper *tuneMapper = [OXmlElementMapper elementClass:[CartoonCharacter class]];
[tuneMapper xpath:@"first" property:@"firstName"];
[tuneMapper xpath:@"last" property:@"lastName"];
OXmlMapper *mapper = [OXmlMapper mapper]; //creates the mapper
[mapper elements:@[ rootMapper, tuneMapper ]]; //contains two element mappers
OXmlReader *reader = [OXmlReader readerWithMapper:mapper]; //creates a reader based on the mapper
reader.context.logReaderStack = NO; //'YES' -> log mapping process
CartoonCharacter *duck = [reader readXmlText:xml]; //reads xml
STAssertEqualObjects(@"Daffy", duck.firstName, @"mapped 'first' element to 'firstName' property"); //test results
STAssertEqualObjects(@"Duck", duck.lastName, @"mapped 'last' element to 'lastName' property");
OXmlWriter *writer = [OXmlWriter writerWithMapper:mapper]; //creates a writer based on mapper
writer.xmlHeader = nil; //doesn't include XML header so we can compare strings
NSString *output = [writer writeXml:duck prettyPrint:NO]; //writes xml
STAssertEqualObjects(xml, output, @"input xml equals output xml");
}
/**
SAXy makes extensive use of the builder design pattern. The builder style in Objective-C takes a little getting
used to, but once mastered allows for concise, flexible mappings. Refer to the 'builder' section of the OXmlMapper
and OXmlElementMapper header files for available methods.
In this example we'll wrap the 'first' and 'last' mappings around the elementClass constructor. Also we'll drop the
unnecessary instance variables. You just have to balance your brackets and the code becomes much easier to read.
Mapping tips: - use Xcode's automatic indentation (^I) to help make the builder code more readable.
- getting wierd errors? verify you're calling a builder method that returns self.
*/
- (void)testBuilderPattern
{
NSString *xml = @"<tune><first>Daffy</first><last>Duck</last></tune>";
OXmlReader *reader = [OXmlReader readerWithMapper: //declare a reader with embedded mapper
[[OXmlMapper mapper] elements:@[ //'elemnts:' builder method
[OXmlElementMapper rootXPath:@"/tune" type:[CartoonCharacter class]]
,
[[[OXmlElementMapper elementClass:[CartoonCharacter class]]
xpath:@"first" property:@"firstName"] //'xpath:property:' builder method
xpath:@"last" property:@"lastName"] //'xpath:property:' builder method
]]
];
CartoonCharacter *duck = [reader readXmlText:xml]; //reads xml
STAssertEqualObjects(@"Daffy", duck.firstName, @"mapped 'first' element to 'firstName' property"); //test results
STAssertEqualObjects(@"Duck", duck.lastName, @"mapped 'last' element to 'lastName' property");
}
/**
The mapper can be configured to read collections of data by calling a 'toMany' constructor. The default container type
is NSMutableArray, but SAXy supports NSDictionary, NSSet, NSOrderedSet, NSArray and their mutable counterparts.
*/
- (void)testReadingLists
{
NSString *xml = @"<tunes><tune><first>Elmer</first><last>Fudd</last></tune><tune><first>Daffy</first><last>Duck</last></tune></tunes>";
//map 'tune' element to CartoonCharacter class:
OXmlMapper *mapper = [[OXmlMapper mapper]
elements:@[
[OXmlElementMapper rootXPath:@"/tunes/tune" toMany:[CartoonCharacter class]]
,
[[[OXmlElementMapper elementClass:[CartoonCharacter class]]
xpath:@"first" property:@"firstName"]
xpath:@"last" property:@"lastName"]
]];
OXmlReader *reader = [OXmlReader readerWithMapper:mapper]; //creates a reader based on the mapper
reader.context.logReaderStack = NO; //'YES' -> log mapping process
NSMutableArray *tunes = [reader readXmlText:xml]; //reads xml
STAssertEquals((NSUInteger)2, [tunes count], @"both Elmer and Daffy"); //test results
CartoonCharacter *elmer = [tunes objectAtIndex:0];
STAssertEqualObjects(@"Elmer", elmer.firstName, @"mapped 'firstName' element to 'firstName' property"); //test elmer
STAssertEqualObjects(@"Fudd", elmer.lastName, @"mapped 'lastName' element to 'lastName' property");
CartoonCharacter *daffy = [tunes objectAtIndex:1];
STAssertEqualObjects(@"Daffy", daffy.firstName, @"mapped 'firstName' element to 'firstName' property"); //test daffy
STAssertEqualObjects(@"Duck", daffy.lastName, @"mapped 'lastName' element to 'lastName' property");
OXmlWriter *writer = [OXmlWriter writerWithMapper:mapper]; //creates a writer based on mapper
writer.xmlHeader = nil; //doesn't include XML header so we can campare result string
NSString *outputXml = [writer writeXml:tunes prettyPrint:NO]; //writes xml using the array
STAssertEqualObjects(xml, outputXml, @"input xml equals output xml");
}
/**
SAXy supports mapping XML attributes to properties. Attributes can be mapped using the 'attribute' builder methods or
by prefixing names with the '@' character in the 'xpath' builder methods. Again refer to the 'builder' section of the
OXmlElementMapper header file for usage.
In addition to the attributes, we'll map Daffy's birthday into the body of the 'tune' element using the 'body'
builder method. Notice that the body mapping is a bit of a special case, it only applies to elements that have attributes
but no child elements.
One last detail is to change the default date mapper, so we don't have to use the default RFC-3339 formatter. We also
reuse the formatter in the writer, by initializing the writer with the reader's context.
*/
- (void)testAttributesAndBodyMappings
{
NSString *xml = @"<tune first='Daffy' last='Duck'>April 4, 1937</tune>";
OXmlMapper *mapper = [[OXmlMapper mapper] elements:@[
[OXmlElementMapper rootXPath:@"/tune" type:[CartoonCharacter class]]
,
[[[[OXmlElementMapper elementClass:[CartoonCharacter class]]
attribute:@"first" property:@"firstName"] //map to attributes
attribute:@"last" property:@"lastName"]
body:@"birthDay"] //map birthDay to element body text
]];
OXmlReader *reader = [OXmlReader readerWithMapper:mapper];
reader.context.logReaderStack = NO; //'YES' -> log mapping process
NSDateFormatter *daffyDateFormatter = [[NSDateFormatter alloc] init]; //configure a new default date formatter
[daffyDateFormatter setLocale:[[NSLocale alloc] initWithLocaleIdentifier:@"en_US"]];
[daffyDateFormatter setDateFormat:@"MMMM d',' yyyy"]; //format: April 4, 1937
[reader.context.transform registerDefaultDateFormatter:daffyDateFormatter]; //register formatter with transform
CartoonCharacter *duck = [reader readXmlText:xml]; //reads xml
STAssertEqualObjects(@"Daffy", duck.firstName, @"mapped 'first' attribute to 'firstName' property"); //test results
STAssertEqualObjects(@"Duck", duck.lastName, @"mapped 'last' attribute to 'lastName' property");
STAssertEqualObjects([daffyDateFormatter dateFromString:@"April 4, 1937"], duck.birthDay, @"mapped element body to 'birthDay' property");
OXmlWriter *writer = [OXmlWriter writerWithMapper:mapper context:reader.context]; //creates writer with reader context's new date formatter
writer.xmlHeader = nil; //doesn't include XML header so we can compare strings
writer.printer.quoteChar = @"'"; //use single quotes for the same reason
NSString *outputXml = [writer writeXml:duck prettyPrint:NO]; //writes xml
STAssertEqualObjects(xml, outputXml, @"input xml equals output xml");
}
/**
Most of the time mappings can be configured using OXmlElementMapper's builder methods. However, when fine-grain
control is required, custom OXmlXPathMapper instances can be created. OXmlXPathMapper defines several important
block properties (i.e. getter, setter, factory and transform functions) that are assigned default functions by
SAXy, unless you set them first.
In this example, we'll override the default KVC setter and getter blocks in OXmlXPathMapper to split a single tag
into two properties. The setter (called by the XML reader) splits the 'name' attribute into segments, assigning
them to their respective properties. The getter (called by the XML writer) concatenates the two properties
back together, returning the value part of the 'name' attribute.
Additionally, two problems have to be avoided:
1) prevent SAXy from validating the 'name' property against the CartoonCharacter class (because it won't find it)
2) avoid writing the 'firstName' and 'lastName' properties to the XML output
The first problem is handled by declaring the 'name' attribute virtual. The second problem can be solved by either
an ignoreProperties:@[@"firstName", @"lastName"] clause. Or, in this case, you could also call the lockMapping
method, effectively telling SAXy to ignore the man behind the curtain (i.e. not engage in any self reflection).
Lastly, this example reverts to the default XML RFC-3339 date formatter. As you may already know, for full ISO 8601
date support, you may want to register a good third-party date formatter (see http://boredzo.org/iso8601unparser/).
*/
- (void)testFineGrainedMapping
{
NSString *xml = @"<tune name='Elmer Fudd'>1940-03-02T00:00:00+0000</tune>";
OXmlMapper *mapper = [[OXmlMapper mapper] elements:@[
[OXmlElementMapper rootXPath:@"/tune" type:[CartoonCharacter class]]
,
[[[[OXmlElementMapper elementClass:[CartoonCharacter class]]
xpathMapper:[[[[OXmlXPathMapper xpath:@"@name" type:[NSString class]] //attribute is indicated with '@' prefix
setter:^(NSString *path, id name, id target, OXContext *ctx) {
CartoonCharacter *tune = target;
NSArray *segments = [name componentsSeparatedByString:@" "];
tune.firstName = [segments count] > 0 ? [segments objectAtIndex:0] : nil;
tune.lastName = [segments count] > 1 ? [segments objectAtIndex:1] : nil;
}]
getter:^(NSString *path, id source, OXContext *ctx) {
CartoonCharacter *tune = source;
return [NSString stringWithFormat:@"%@ %@", tune.firstName, tune.lastName];
}]
isVirtualProperty] //prevent validation against CartoonCharacter class
]
body:@"birthDay"]
ignoreProperties:@[@"firstName", @"lastName"]] //doesn't write (or read) these properties
//lockMapping] //prevents SAXy from discovering and writing firstName and lastName properties
]];
OXmlReader *reader = [OXmlReader readerWithMapper:mapper];
reader.context.logReaderStack = NO; //'YES' -> log mapping process
CartoonCharacter *fudd = [reader readXmlText:xml]; //reads xml
STAssertEqualObjects(@"Elmer", fudd.firstName, @"mapped 'first' attribute to 'firstName' property"); //test results
STAssertEqualObjects(@"Fudd", fudd.lastName, @"mapped 'last' attribute to 'lastName' property");
NSDateFormatter *formatter = [reader.context.transform defaultDateFormatter]; //dig out default date formatter
STAssertEqualObjects([formatter dateFromString:@"1940-03-02T00:00:00+0000"], fudd.birthDay, @"mapped element body to 'birthDay' property");
OXmlWriter *writer = [OXmlWriter writerWithMapper:mapper]; //creates a writer based on mapper
writer.xmlHeader = nil; //doesn't include XML header so we can compare strings
writer.printer.quoteChar = @"'"; //use single quotes for the same reason
NSString *output = [writer writeXml:fudd prettyPrint:NO]; //writes xml
STAssertEqualObjects(xml, output, @"input xml equals output xml");
}
/**
SAXy has full XML naemespace support including support for prefixed and default (non-prefixed) namespaces.
Namespaces can be declared:
1) per mapping - in the OXmlMapper constructor
2) per class - in the OXmlElementMapper constructor
3) per property - using OXmlElementMapper's switchToNamespaceURI builder method
This example demonstrates how to declare a prefixed namespace for an entire mapping.
See OXiTunesTests.m for an example of a complex, mixed namespace mapping on a RSS document.
*/
- (void)testNamespaceSupport
{
NSString *xml = @"<l:tune xmlns:l='disney.com/luneytunes'><l:first>Elmer</l:first><l:last>Fudd</l:last></l:tune>";
//map 'tune' element to CartoonCharacter class:
OXmlMapper *mapper = [[OXmlMapper mapperWithRootNamespace:@"disney.com/luneytunes" recommendedPrefix:@"l"]
elements:@[
[OXmlElementMapper rootXPath:@"/tune" type:[CartoonCharacter class]]
,
[[[OXmlElementMapper elementClass:[CartoonCharacter class]]
xpath:@"first" property:@"firstName"]
xpath:@"last" property:@"lastName"]
]];
OXmlReader *reader = [OXmlReader readerWithMapper:mapper]; //creates a reader based on the mapper
CartoonCharacter *fudd = [reader readXmlText:xml]; //reads xml
STAssertEqualObjects(@"Elmer", fudd.firstName, @"mapped 'first' attribute to 'firstName' property"); //test results
STAssertEqualObjects(@"Fudd", fudd.lastName, @"mapped 'last' attribute to 'lastName' property");
OXmlWriter *writer = [OXmlWriter writerWithMapper:mapper]; //creates a writer based on mapper
writer.xmlHeader = nil; //doesn't include XML header so we can compare strings
writer.printer.quoteChar = @"'"; //use single quotes for the same reason
NSString *output = [writer writeXml:fudd prettyPrint:NO]; //writes xml
STAssertEqualObjects(xml, output, @"input xml equals output xml");
}
@end
//
// Copyright (c) 2013 Outsource Cafe, Inc. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//