| 1 | import YamlTest |
|---|
| 2 | from here import flushLeft |
|---|
| 3 | from test import assertEquals, assertError |
|---|
| 4 | from TestPullParser import mockParser, Loader |
|---|
| 5 | from yaml import load |
|---|
| 6 | |
|---|
| 7 | """ |
|---|
| 8 | Part of the parse/pull experimental code. This does a schema-driven |
|---|
| 9 | validating parse of YAML documents, but it's built on top of a |
|---|
| 10 | crufty interim solution. |
|---|
| 11 | |
|---|
| 12 | The schema-driven parser requires a pull parser interface. Ideally |
|---|
| 13 | a pull-parser would be pulling nodes from a YAML document on an |
|---|
| 14 | as-needed basis, and this is the eventual goal. But, I don't have |
|---|
| 15 | a pull parser yet, so I simulate one by reading in the entire YAML |
|---|
| 16 | document into a Python data structure, then I do a push-based dump |
|---|
| 17 | of the data structure to a mock emitter that stores up a list of |
|---|
| 18 | parser events that a mock parser then serves up to the schema-driven |
|---|
| 19 | parser on an as-needed basis. Sounds complex, but there's really not |
|---|
| 20 | much code involved. |
|---|
| 21 | |
|---|
| 22 | Nothing fancy is supported yet--just lists, dictionaries, and scalars; |
|---|
| 23 | no aliases, class transformations, multiple docs, etc. Also, we lose |
|---|
| 24 | the sort order on map keys, so you will notice that all the examples |
|---|
| 25 | have alphabetically sorted keys. |
|---|
| 26 | |
|---|
| 27 | Also, once we go to a true pull parser, we could have more metadata, |
|---|
| 28 | such as line numbers for nodes, attached comments, etc., that can |
|---|
| 29 | help with error reporting and round-tripping issues. |
|---|
| 30 | """ |
|---|
| 31 | |
|---|
| 32 | testCases = """ |
|---|
| 33 | - |
|---|
| 34 | data: | |
|---|
| 35 | --- foo |
|---|
| 36 | schema: |
|---|
| 37 | type: scalar |
|---|
| 38 | - |
|---|
| 39 | data: | |
|---|
| 40 | --- foo |
|---|
| 41 | schema: |
|---|
| 42 | type: seq |
|---|
| 43 | error: | |
|---|
| 44 | Wanted seq, got scalar |
|---|
| 45 | - |
|---|
| 46 | data: &list123 | |
|---|
| 47 | --- |
|---|
| 48 | - 1 |
|---|
| 49 | - 2 |
|---|
| 50 | - 3 |
|---|
| 51 | schema: |
|---|
| 52 | type: seq |
|---|
| 53 | child: |
|---|
| 54 | type: scalar |
|---|
| 55 | - |
|---|
| 56 | data: *list123 |
|---|
| 57 | schema: |
|---|
| 58 | type: seq |
|---|
| 59 | max: 2 |
|---|
| 60 | child: |
|---|
| 61 | type: scalar |
|---|
| 62 | error: | |
|---|
| 63 | Seq has max 2 elements |
|---|
| 64 | - |
|---|
| 65 | data: | |
|---|
| 66 | --- |
|---|
| 67 | city: New Orleans |
|---|
| 68 | state: LA |
|---|
| 69 | street: Bourbon |
|---|
| 70 | schema: &StreetCityState |
|---|
| 71 | type: map |
|---|
| 72 | items: |
|---|
| 73 | - name: city |
|---|
| 74 | value: |
|---|
| 75 | type: scalar |
|---|
| 76 | - name: state |
|---|
| 77 | value: |
|---|
| 78 | type: scalar |
|---|
| 79 | - name: street |
|---|
| 80 | value: |
|---|
| 81 | type: scalar |
|---|
| 82 | - |
|---|
| 83 | data: | |
|---|
| 84 | --- |
|---|
| 85 | city: New Orleans |
|---|
| 86 | state: LA |
|---|
| 87 | where ya got ya shoes: on ya feet, on Bourbon St. |
|---|
| 88 | schema: *StreetCityState |
|---|
| 89 | error: | |
|---|
| 90 | Expected key 'street', got 'where ya got ya shoes' |
|---|
| 91 | - |
|---|
| 92 | data: | |
|---|
| 93 | --- |
|---|
| 94 | banana: yellow |
|---|
| 95 | carrot: orange |
|---|
| 96 | people: |
|---|
| 97 | - fname: al |
|---|
| 98 | salary: 44 |
|---|
| 99 | - fname: bob |
|---|
| 100 | salary: 33 |
|---|
| 101 | schema: |
|---|
| 102 | type: map |
|---|
| 103 | items: |
|---|
| 104 | - name: banana |
|---|
| 105 | value: |
|---|
| 106 | type: scalar |
|---|
| 107 | - name: carrot |
|---|
| 108 | value: |
|---|
| 109 | type: scalar |
|---|
| 110 | - name: people |
|---|
| 111 | value: |
|---|
| 112 | type: seq |
|---|
| 113 | child: |
|---|
| 114 | type: map |
|---|
| 115 | items: |
|---|
| 116 | - name: fname |
|---|
| 117 | value: |
|---|
| 118 | type: scalar |
|---|
| 119 | - name: salary |
|---|
| 120 | value: |
|---|
| 121 | type: scalar |
|---|
| 122 | """ |
|---|
| 123 | |
|---|
| 124 | class ValidatingLoader: |
|---|
| 125 | def load(self, data, schema): |
|---|
| 126 | self.simulateParser(data) |
|---|
| 127 | return self.loadData(schema) |
|---|
| 128 | |
|---|
| 129 | def loadData(self, schema): |
|---|
| 130 | typ = self.parser.getType() |
|---|
| 131 | return self._load(typ, schema) |
|---|
| 132 | |
|---|
| 133 | def _load(self, typ, schema): |
|---|
| 134 | if typ != schema['type']: |
|---|
| 135 | raise Exception("Wanted %s, got %s\n" % (schema['type'], typ)) |
|---|
| 136 | if typ == 'seq': |
|---|
| 137 | return self._loadSeq(schema) |
|---|
| 138 | if typ == 'map': |
|---|
| 139 | return self._loadMap(schema) |
|---|
| 140 | else: |
|---|
| 141 | return self.parser.getScalar() |
|---|
| 142 | |
|---|
| 143 | def _loadSeq(self, schema): |
|---|
| 144 | results = [] |
|---|
| 145 | cnt = 0 |
|---|
| 146 | max = schema.get('max', None) |
|---|
| 147 | schema = schema['child'] |
|---|
| 148 | while 1: |
|---|
| 149 | typ = self.parser.getType() |
|---|
| 150 | if typ is None: |
|---|
| 151 | return results |
|---|
| 152 | else: |
|---|
| 153 | cnt += 1 |
|---|
| 154 | self.checkMax(cnt, max) |
|---|
| 155 | results.append(self._load(typ, schema)) |
|---|
| 156 | |
|---|
| 157 | def _loadMap(self, schema): |
|---|
| 158 | results = {} |
|---|
| 159 | for item in schema['items']: |
|---|
| 160 | self.parser.getType() |
|---|
| 161 | name = self.parser.getScalar() |
|---|
| 162 | self.checkName(name, item) |
|---|
| 163 | value = self.loadData(item['value']) |
|---|
| 164 | results[name] = value |
|---|
| 165 | self.parser.getType() |
|---|
| 166 | return results |
|---|
| 167 | |
|---|
| 168 | def checkMax(self, cnt, max): |
|---|
| 169 | if max is not None and cnt > max: |
|---|
| 170 | raise Exception("Seq has max %d elements\n" % max) |
|---|
| 171 | |
|---|
| 172 | def checkName(self, name, item): |
|---|
| 173 | if name != item['name']: |
|---|
| 174 | raise Exception("Expected key '%s', got '%s'\n" % \ |
|---|
| 175 | (item['name'], name)) |
|---|
| 176 | |
|---|
| 177 | def simulateParser(self, data): |
|---|
| 178 | # This is the huge hack to work around |
|---|
| 179 | # not having a true pull parser |
|---|
| 180 | self.parser = mockParser(oldYamlLoad(data)) |
|---|
| 181 | |
|---|
| 182 | def testRoundTrip(data, schema): |
|---|
| 183 | expected = oldYamlLoad(data) |
|---|
| 184 | obj = ValidatingLoader().load(data, schema) |
|---|
| 185 | assertEquals(expected, obj) |
|---|
| 186 | |
|---|
| 187 | def oldYamlLoad(data): |
|---|
| 188 | return load(data).next() |
|---|
| 189 | |
|---|
| 190 | def testOneCase(test): |
|---|
| 191 | data = test['data'] |
|---|
| 192 | schema = test['schema'] |
|---|
| 193 | if test.has_key('error'): |
|---|
| 194 | assertError(lambda: testRoundTrip(data, schema), |
|---|
| 195 | test['error']) |
|---|
| 196 | else: |
|---|
| 197 | testRoundTrip(data, schema) |
|---|
| 198 | |
|---|
| 199 | class Test(YamlTest.YamlTest): |
|---|
| 200 | def testFromYaml(self): |
|---|
| 201 | for test in load(testCases).next(): |
|---|
| 202 | testOneCase(test) |
|---|
| 203 | |
|---|
| 204 | if __name__ == '__main__': |
|---|
| 205 | import unittest |
|---|
| 206 | unittest.main() |
|---|