summaryrefslogtreecommitdiffstats
path: root/chrome/common/extensions/docs/server2/schema_processor.py
blob: af0cdd70a2a74824fb309551f784e2610df74893 (plain)
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
# Copyright 2013 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

from collections import defaultdict, Mapping
import traceback

from third_party.json_schema_compiler import json_parse, idl_schema, idl_parser
from reference_resolver import ReferenceResolver
from compiled_file_system import CompiledFileSystem

class SchemaProcessorForTest(object):
  '''Fake SchemaProcessor class. Returns the original schema, without
  processing.
  '''
  def Process(self, path, file_data):
    if path.endswith('.idl'):
      idl = idl_schema.IDLSchema(idl_parser.IDLParser().ParseData(file_data))
      # Wrap the result in a list so that it behaves like JSON API data.
      return [idl.process()[0]]
    return json_parse.Parse(file_data)

class SchemaProcessorFactoryForTest(object):
  '''Returns a fake SchemaProcessor class to be used for testing.
  '''
  def Create(self, retain_inlined_types):
    return SchemaProcessorForTest()


class SchemaProcessorFactory(object):
  '''Factory for creating the schema processing utility.
  '''
  def __init__(self,
               reference_resolver,
               api_models,
               features_bundle,
               compiled_fs_factory,
               file_system):
    self._reference_resolver = reference_resolver
    self._api_models = api_models
    self._features_bundle = features_bundle
    self._compiled_fs_factory = compiled_fs_factory
    self._file_system = file_system

  def Create(self, retain_inlined_types):
    return SchemaProcessor(self._reference_resolver.Get(),
                           self._api_models.Get(),
                           self._features_bundle.Get(),
                           self._compiled_fs_factory,
                           self._file_system,
                           retain_inlined_types)


class SchemaProcessor(object):
  '''Helper for parsing the API schema.
  '''
  def __init__(self,
               reference_resolver,
               api_models,
               features_bundle,
               compiled_fs_factory,
               file_system,
               retain_inlined_types):
    self._reference_resolver = reference_resolver
    self._api_models = api_models
    self._features_bundle = features_bundle
    self._retain_inlined_types = retain_inlined_types
    self._compiled_file_system = compiled_fs_factory.Create(
        file_system, self.Process, SchemaProcessor, category='json-cache')
    self._api_stack = []

  def _RemoveNoDocs(self, item):
    '''Removes nodes that should not be rendered from an API schema.
    '''
    if json_parse.IsDict(item):
      if item.get('nodoc', False):
        return True
      for key, value in item.items():
        if self._RemoveNoDocs(value):
          del item[key]
    elif type(item) == list:
      to_remove = []
      for i in item:
        if self._RemoveNoDocs(i):
          to_remove.append(i)
      for i in to_remove:
        item.remove(i)
    return False


  def _DetectInlineableTypes(self, schema):
    '''Look for documents that are only referenced once and mark them as inline.
    Actual inlining is done by _InlineDocs.
    '''
    if not schema.get('types'):
      return

    ignore = frozenset(('value', 'choices'))
    refcounts = defaultdict(int)
    # Use an explicit stack instead of recursion.
    stack = [schema]

    while stack:
      node = stack.pop()
      if isinstance(node, list):
        stack.extend(node)
      elif isinstance(node, Mapping):
        if '$ref' in node:
          refcounts[node['$ref']] += 1
        stack.extend(v for k, v in node.iteritems() if k not in ignore)

    for type_ in schema['types']:
      if not 'noinline_doc' in type_:
        if refcounts[type_['id']] == 1:
          type_['inline_doc'] = True


  def _InlineDocs(self, schema):
    '''Replace '$ref's that refer to inline_docs with the json for those docs.
    If |retain_inlined_types| is False, then the inlined nodes are removed
    from the schema.
    '''
    inline_docs = {}
    types_without_inline_doc = []
    internal_api = False

    api_features = self._features_bundle.GetAPIFeatures().Get()
    # We don't want to inline the events API, as it's handled differently
    # Also, the webviewTag API is handled differently, as it only exists
    # for the purpose of documentation, it's not a true internal api
    namespace = schema.get('namespace', '')
    if namespace != 'events' and namespace != 'webviewTag':
      internal_api = api_features.get(schema.get('namespace', ''), {}).get(
          'internal', False)

    api_refs = set()
    # Gather refs to internal APIs
    def gather_api_refs(node):
      if isinstance(node, list):
        for i in node:
          gather_api_refs(i)
      elif isinstance(node, Mapping):
        ref = node.get('$ref')
        if ref:
          api_refs.add(ref)
        for k, v in node.iteritems():
          gather_api_refs(v)
    gather_api_refs(schema)

    if len(api_refs) > 0:
      api_list = self._api_models.GetNames()
      api_name = schema.get('namespace', '')
      self._api_stack.append(api_name)
      for api in self._api_stack:
        if api in api_list:
          api_list.remove(api)
      for ref in api_refs:
        model, node_info = self._reference_resolver.GetRefModel(ref, api_list)
        if model and api_features.get(model.name, {}).get('internal', False):
          category, name = node_info
          for ref_schema in self._compiled_file_system.GetFromFile(
              model.source_file).Get():
            if category == 'type':
              for type_json in ref_schema.get('types'):
                if type_json['id'] == name:
                  inline_docs[ref] = type_json
            elif category == 'event':
              for type_json in ref_schema.get('events'):
                if type_json['name'] == name:
                  inline_docs[ref] = type_json
      self._api_stack.remove(api_name)

    types = schema.get('types')
    if types:
      # Gather the types with inline_doc.
      for type_ in types:
        if type_.get('inline_doc'):
          inline_docs[type_['id']] = type_
          if not self._retain_inlined_types:
            for k in ('description', 'id', 'inline_doc'):
              type_.pop(k, None)
        elif internal_api:
          inline_docs[type_['id']] = type_
          # For internal apis that are not inline_doc we want to retain them
          # in the schema (i.e. same behaviour as remain_inlined_types)
          types_without_inline_doc.append(type_)
        else:
          types_without_inline_doc.append(type_)
      if not self._retain_inlined_types:
        schema['types'] = types_without_inline_doc

    def apply_inline(node):
      if isinstance(node, list):
        for i in node:
          apply_inline(i)
      elif isinstance(node, Mapping):
        ref = node.get('$ref')
        if ref and ref in inline_docs:
          node.update(inline_docs[ref])
          del node['$ref']
        for k, v in node.iteritems():
          apply_inline(v)

    apply_inline(schema)


  def Process(self, path, file_data):
    '''Parses |file_data| using a method determined by checking the
    extension of the file at the given |path|. Then, trims 'nodoc' and if
    |self.retain_inlined_types| is given and False, removes inlineable types
    from the parsed schema data.
    '''
    def trim_and_inline(schema, is_idl=False):
      '''Modifies an API schema in place by removing nodes that shouldn't be
      documented and inlining schema types that are only referenced once.
      '''
      if self._RemoveNoDocs(schema):
        # A return of True signifies that the entire schema should not be
        # documented. Otherwise, only nodes that request 'nodoc' are removed.
        return None
      if is_idl:
        self._DetectInlineableTypes(schema)
      self._InlineDocs(schema)
      return schema

    if path.endswith('.idl'):
      idl = idl_schema.IDLSchema(
          idl_parser.IDLParser().ParseData(file_data))
      # Wrap the result in a list so that it behaves like JSON API data.
      return [trim_and_inline(idl.process()[0], is_idl=True)]

    try:
      schemas = json_parse.Parse(file_data)
    except:
      raise ValueError('Cannot parse "%s" as JSON:\n%s' %
                       (path, traceback.format_exc()))
    for schema in schemas:
      # Schemas could consist of one API schema (data for a specific API file)
      # or multiple (data from extension_api.json).
      trim_and_inline(schema)
    return schemas