Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / chrome / common / extensions / docs / server2 / schema_processor.py
blobaf0cdd70a2a74824fb309551f784e2610df74893
1 # Copyright 2013 The Chromium Authors. All rights reserved.
2 # Use of this source code is governed by a BSD-style license that can be
3 # found in the LICENSE file.
5 from collections import defaultdict, Mapping
6 import traceback
8 from third_party.json_schema_compiler import json_parse, idl_schema, idl_parser
9 from reference_resolver import ReferenceResolver
10 from compiled_file_system import CompiledFileSystem
12 class SchemaProcessorForTest(object):
13 '''Fake SchemaProcessor class. Returns the original schema, without
14 processing.
15 '''
16 def Process(self, path, file_data):
17 if path.endswith('.idl'):
18 idl = idl_schema.IDLSchema(idl_parser.IDLParser().ParseData(file_data))
19 # Wrap the result in a list so that it behaves like JSON API data.
20 return [idl.process()[0]]
21 return json_parse.Parse(file_data)
23 class SchemaProcessorFactoryForTest(object):
24 '''Returns a fake SchemaProcessor class to be used for testing.
25 '''
26 def Create(self, retain_inlined_types):
27 return SchemaProcessorForTest()
30 class SchemaProcessorFactory(object):
31 '''Factory for creating the schema processing utility.
32 '''
33 def __init__(self,
34 reference_resolver,
35 api_models,
36 features_bundle,
37 compiled_fs_factory,
38 file_system):
39 self._reference_resolver = reference_resolver
40 self._api_models = api_models
41 self._features_bundle = features_bundle
42 self._compiled_fs_factory = compiled_fs_factory
43 self._file_system = file_system
45 def Create(self, retain_inlined_types):
46 return SchemaProcessor(self._reference_resolver.Get(),
47 self._api_models.Get(),
48 self._features_bundle.Get(),
49 self._compiled_fs_factory,
50 self._file_system,
51 retain_inlined_types)
54 class SchemaProcessor(object):
55 '''Helper for parsing the API schema.
56 '''
57 def __init__(self,
58 reference_resolver,
59 api_models,
60 features_bundle,
61 compiled_fs_factory,
62 file_system,
63 retain_inlined_types):
64 self._reference_resolver = reference_resolver
65 self._api_models = api_models
66 self._features_bundle = features_bundle
67 self._retain_inlined_types = retain_inlined_types
68 self._compiled_file_system = compiled_fs_factory.Create(
69 file_system, self.Process, SchemaProcessor, category='json-cache')
70 self._api_stack = []
72 def _RemoveNoDocs(self, item):
73 '''Removes nodes that should not be rendered from an API schema.
74 '''
75 if json_parse.IsDict(item):
76 if item.get('nodoc', False):
77 return True
78 for key, value in item.items():
79 if self._RemoveNoDocs(value):
80 del item[key]
81 elif type(item) == list:
82 to_remove = []
83 for i in item:
84 if self._RemoveNoDocs(i):
85 to_remove.append(i)
86 for i in to_remove:
87 item.remove(i)
88 return False
91 def _DetectInlineableTypes(self, schema):
92 '''Look for documents that are only referenced once and mark them as inline.
93 Actual inlining is done by _InlineDocs.
94 '''
95 if not schema.get('types'):
96 return
98 ignore = frozenset(('value', 'choices'))
99 refcounts = defaultdict(int)
100 # Use an explicit stack instead of recursion.
101 stack = [schema]
103 while stack:
104 node = stack.pop()
105 if isinstance(node, list):
106 stack.extend(node)
107 elif isinstance(node, Mapping):
108 if '$ref' in node:
109 refcounts[node['$ref']] += 1
110 stack.extend(v for k, v in node.iteritems() if k not in ignore)
112 for type_ in schema['types']:
113 if not 'noinline_doc' in type_:
114 if refcounts[type_['id']] == 1:
115 type_['inline_doc'] = True
118 def _InlineDocs(self, schema):
119 '''Replace '$ref's that refer to inline_docs with the json for those docs.
120 If |retain_inlined_types| is False, then the inlined nodes are removed
121 from the schema.
123 inline_docs = {}
124 types_without_inline_doc = []
125 internal_api = False
127 api_features = self._features_bundle.GetAPIFeatures().Get()
128 # We don't want to inline the events API, as it's handled differently
129 # Also, the webviewTag API is handled differently, as it only exists
130 # for the purpose of documentation, it's not a true internal api
131 namespace = schema.get('namespace', '')
132 if namespace != 'events' and namespace != 'webviewTag':
133 internal_api = api_features.get(schema.get('namespace', ''), {}).get(
134 'internal', False)
136 api_refs = set()
137 # Gather refs to internal APIs
138 def gather_api_refs(node):
139 if isinstance(node, list):
140 for i in node:
141 gather_api_refs(i)
142 elif isinstance(node, Mapping):
143 ref = node.get('$ref')
144 if ref:
145 api_refs.add(ref)
146 for k, v in node.iteritems():
147 gather_api_refs(v)
148 gather_api_refs(schema)
150 if len(api_refs) > 0:
151 api_list = self._api_models.GetNames()
152 api_name = schema.get('namespace', '')
153 self._api_stack.append(api_name)
154 for api in self._api_stack:
155 if api in api_list:
156 api_list.remove(api)
157 for ref in api_refs:
158 model, node_info = self._reference_resolver.GetRefModel(ref, api_list)
159 if model and api_features.get(model.name, {}).get('internal', False):
160 category, name = node_info
161 for ref_schema in self._compiled_file_system.GetFromFile(
162 model.source_file).Get():
163 if category == 'type':
164 for type_json in ref_schema.get('types'):
165 if type_json['id'] == name:
166 inline_docs[ref] = type_json
167 elif category == 'event':
168 for type_json in ref_schema.get('events'):
169 if type_json['name'] == name:
170 inline_docs[ref] = type_json
171 self._api_stack.remove(api_name)
173 types = schema.get('types')
174 if types:
175 # Gather the types with inline_doc.
176 for type_ in types:
177 if type_.get('inline_doc'):
178 inline_docs[type_['id']] = type_
179 if not self._retain_inlined_types:
180 for k in ('description', 'id', 'inline_doc'):
181 type_.pop(k, None)
182 elif internal_api:
183 inline_docs[type_['id']] = type_
184 # For internal apis that are not inline_doc we want to retain them
185 # in the schema (i.e. same behaviour as remain_inlined_types)
186 types_without_inline_doc.append(type_)
187 else:
188 types_without_inline_doc.append(type_)
189 if not self._retain_inlined_types:
190 schema['types'] = types_without_inline_doc
192 def apply_inline(node):
193 if isinstance(node, list):
194 for i in node:
195 apply_inline(i)
196 elif isinstance(node, Mapping):
197 ref = node.get('$ref')
198 if ref and ref in inline_docs:
199 node.update(inline_docs[ref])
200 del node['$ref']
201 for k, v in node.iteritems():
202 apply_inline(v)
204 apply_inline(schema)
207 def Process(self, path, file_data):
208 '''Parses |file_data| using a method determined by checking the
209 extension of the file at the given |path|. Then, trims 'nodoc' and if
210 |self.retain_inlined_types| is given and False, removes inlineable types
211 from the parsed schema data.
213 def trim_and_inline(schema, is_idl=False):
214 '''Modifies an API schema in place by removing nodes that shouldn't be
215 documented and inlining schema types that are only referenced once.
217 if self._RemoveNoDocs(schema):
218 # A return of True signifies that the entire schema should not be
219 # documented. Otherwise, only nodes that request 'nodoc' are removed.
220 return None
221 if is_idl:
222 self._DetectInlineableTypes(schema)
223 self._InlineDocs(schema)
224 return schema
226 if path.endswith('.idl'):
227 idl = idl_schema.IDLSchema(
228 idl_parser.IDLParser().ParseData(file_data))
229 # Wrap the result in a list so that it behaves like JSON API data.
230 return [trim_and_inline(idl.process()[0], is_idl=True)]
232 try:
233 schemas = json_parse.Parse(file_data)
234 except:
235 raise ValueError('Cannot parse "%s" as JSON:\n%s' %
236 (path, traceback.format_exc()))
237 for schema in schemas:
238 # Schemas could consist of one API schema (data for a specific API file)
239 # or multiple (data from extension_api.json).
240 trim_and_inline(schema)
241 return schemas