summaryrefslogtreecommitdiffstats
path: root/tools/json_schema_compiler/compiler.py
blob: ae9237336f41af191a39fe1dbe87791ee3e16c09 (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
#!/usr/bin/env python
# Copyright (c) 2012 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.
"""Generator for C++ structs from api json files.

The purpose of this tool is to remove the need for hand-written code that
converts to and from base::Value types when receiving javascript api calls.
Originally written for generating code for extension apis. Reference schemas
are in chrome/common/extensions/api.

Usage example:
  compiler.py --root /home/Work/src --namespace extensions windows.json
    tabs.json
  compiler.py --destdir gen --root /home/Work/src
    --namespace extensions windows.json tabs.json
"""

import cc_generator
import cpp_type_generator
import h_generator
import idl_schema
import json_schema
import model
import schema_bundle_generator

import optparse
import os.path
import sys

def load_schema(schema):
  schema_filename, schema_extension = os.path.splitext(schema)

  if schema_extension == '.json':
    api_defs = json_schema.Load(schema)
  elif schema_extension == '.idl':
    api_defs = idl_schema.Load(schema)
  else:
    sys.exit("Did not recognize file extension %s for schema %s" %
             (schema_extension, schema))
  if len(api_defs) != 1:
    sys.exit("File %s has multiple schemas. Files are only allowed to contain a"
             " single schema." % schema)

  return api_defs

def handle_single_schema(filename, dest_dir, root, root_namespace):
  schema = os.path.normpath(filename)
  schema_filename, schema_extension = os.path.splitext(schema)
  path, short_filename = os.path.split(schema_filename)
  api_defs = json_schema.DeleteNocompileNodes(load_schema(schema))

  api_model = model.Model()

  for target_namespace in api_defs:
    referenced_schemas = target_namespace.get('dependencies', [])
    # Load type dependencies into the model.
    # TODO(miket): do we need this in IDL?
    for referenced_schema in referenced_schemas:
      split_schema = referenced_schema.split(':', 1)
      if len(split_schema) > 1:
        if split_schema[0] != 'api':
          continue
        else:
          referenced_schema = split_schema[1]

      referenced_schema_path = os.path.join(
          os.path.dirname(schema), referenced_schema + '.json')
      referenced_api_defs = json_schema.Load(referenced_schema_path)

      for namespace in referenced_api_defs:
        api_model.AddNamespace(namespace,
            os.path.relpath(referenced_schema_path, opts.root))

    # Gets the relative path from opts.root to the schema to correctly determine
    # the include path.
    relpath = os.path.relpath(schema, opts.root)
    namespace = api_model.AddNamespace(target_namespace, relpath)
    if not namespace:
      continue

    if short_filename != namespace.unix_name:
      sys.exit("Filename %s is illegal. Name files using unix_hacker style." %
               filename)

    # The output filename must match the input filename for gyp to deal with it
    # properly.
    out_file = namespace.unix_name
    type_generator = cpp_type_generator.CppTypeGenerator(
        root_namespace, namespace, namespace.unix_name)
    for referenced_namespace in api_model.namespaces.values():
      if referenced_namespace == namespace:
        continue
      type_generator.AddNamespace(
          referenced_namespace,
          referenced_namespace.unix_name)

    h_code = (h_generator.HGenerator(namespace, type_generator)
        .Generate().Render())
    cc_code = (cc_generator.CCGenerator(namespace, type_generator)
        .Generate().Render())

    if dest_dir:
      with open(
          os.path.join(dest_dir, namespace.source_file_dir, out_file + '.cc'),
          'w') as cc_file:
        cc_file.write(cc_code)
      with open(
          os.path.join(dest_dir, namespace.source_file_dir, out_file + '.h'),
          'w') as h_file:
        h_file.write(h_code)
    else:
      print '%s.h' % out_file
      print
      print h_code
      print
      print '%s.cc' % out_file
      print
      print cc_code

def handle_bundle_schema(filenames, dest_dir, root, root_namespace):
  # Merge the source files into a single list of schemas.
  api_defs = []
  for filename in filenames:
    schema = os.path.normpath(filename)
    schema_filename, schema_extension = os.path.splitext(schema)
    api_defs.extend(load_schema(schema))

  api_model = model.Model()
  relpath = os.path.relpath(os.path.normpath(filenames[0]), root)

  for target_namespace, schema_filename in zip(api_defs, filenames):
    namespace = api_model.AddNamespace(target_namespace, relpath)
    path, filename = os.path.split(schema_filename)
    short_filename, extension = os.path.splitext(filename)

    # Filenames are checked against the unix_names of the namespaces they
    # generate because the gyp uses the names of the JSON files to generate
    # the names of the .cc and .h files. We want these to be using unix_names.
    if namespace.unix_name != short_filename:
      sys.exit("Filename %s is illegal. Name files using unix_hacker style." %
               schema_filename)

  type_generator = cpp_type_generator.CppTypeGenerator(root_namespace)
  for referenced_namespace in api_model.namespaces.values():
    type_generator.AddNamespace(
        referenced_namespace,
        referenced_namespace.unix_name)

  generator = schema_bundle_generator.SchemaBundleGenerator(
      api_model, api_defs, type_generator)
  api_h_code = generator.GenerateAPIHeader().Render()
  schemas_h_code = generator.GenerateSchemasHeader().Render()
  schemas_cc_code = generator.GenerateSchemasCC().Render()

  if dest_dir:
    basedir = os.path.join(dest_dir, 'chrome/common/extensions/api')
    with open(os.path.join(basedir, 'generated_api.h'), 'w') as h_file:
      h_file.write(api_h_code)
    with open(os.path.join(basedir, 'generated_schemas.h'), 'w') as h_file:
      h_file.write(schemas_h_code)
    with open(os.path.join(basedir, 'generated_schemas.cc'), 'w') as cc_file:
      cc_file.write(schemas_cc_code)
  else:
    print 'generated_api.h'
    print
    print api_h_code
    print
    print 'generated_schemas.h'
    print
    print schemas_h_code
    print
    print 'generated_schemas.cc'
    print
    print schemas_cc_code

if __name__ == '__main__':
  parser = optparse.OptionParser(
      description='Generates a C++ model of an API from JSON schema',
      usage='usage: %prog [option]... schema')
  parser.add_option('-r', '--root', default='.',
      help='logical include root directory. Path to schema files from specified'
      'dir will be the include path.')
  parser.add_option('-d', '--destdir',
      help='root directory to output generated files.')
  parser.add_option('-n', '--namespace', default='generated_api_schemas',
      help='C++ namespace for generated files. e.g extensions::api.')
  parser.add_option('-b', '--bundle', action="store_true", help=
'''if supplied, causes compiler to generate bundle files for the given set of
source files.''')

  (opts, args) = parser.parse_args()

  if not args:
    sys.exit(0) # This is OK as a no-op
  dest_dir = opts.destdir
  root_namespace = opts.namespace

  if opts.bundle:
    handle_bundle_schema(args, dest_dir, opts.root, root_namespace)
  else:
    handle_single_schema(args[0], dest_dir, opts.root, root_namespace)