schema-parser.h 11.2 KB
Newer Older
Kenton Varda's avatar
Kenton Varda committed
1 2
// Copyright (c) 2013-2014 Sandstorm Development Group, Inc. and contributors
// Licensed under the MIT License:
3
//
Kenton Varda's avatar
Kenton Varda committed
4 5 6 7 8 9
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
10
//
Kenton Varda's avatar
Kenton Varda committed
11 12
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
13
//
Kenton Varda's avatar
Kenton Varda committed
14 15 16 17 18 19 20
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
21

22
#pragma once
23

24
#if defined(__GNUC__) && !defined(CAPNP_HEADER_WARNINGS)
25 26 27
#pragma GCC system_header
#endif

28 29
#include "schema-loader.h"
#include <kj/string.h>
30
#include <kj/filesystem.h>
31 32 33 34 35 36 37 38

namespace capnp {

class ParsedSchema;
class SchemaFile;

class SchemaParser {
  // Parses `.capnp` files to produce `Schema` objects.
39 40
  //
  // This class is thread-safe, hence all its methods are const.
41 42 43 44 45

public:
  SchemaParser();
  ~SchemaParser() noexcept(false);

46 47 48
  ParsedSchema parseFromDirectory(
      const kj::ReadableDirectory& baseDir, kj::Path path,
      kj::ArrayPtr<const kj::ReadableDirectory* const> importPath) const;
49 50 51 52 53 54 55 56 57 58 59 60
  // Parse a file from the KJ filesystem API.  Throws an exception if the file dosen't exist.
  //
  // `baseDir` and `path` are used together to resolve relative imports. `path` is the source
  // file's path within `baseDir`. Relative imports will be interpreted relative to `path` and
  // will be opened using `baseDir`. Note that the KJ filesystem API prohibits "breaking out" of
  // a directory using "..", so relative imports will be restricted to children of `baseDir`.
  //
  // `importPath` is used for absolute imports (imports that start with a '/'). Each directory in
  // the array will be searched in order until a file is found.
  //
  // All `ReadableDirectory` objects must remain valid until the `SchemaParser` is destroyed. Also,
  // the `importPath` array must remain valid. `path` will be copied; it need not remain valid.
61 62
  //
  // This method is a shortcut, equivalent to:
63
  //     parser.parseFromDirectory(SchemaFile::newDiskFile(baseDir, path, importPath))`;
64 65 66 67 68
  //
  // This method throws an exception if any errors are encountered in the file or in anything the
  // file depends on.  Note that merely importing another file does not count as a dependency on
  // anything in the imported file -- only the imported types which are actually used are
  // "dependencies".
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
  //
  // Hint: Use kj::newDiskFilesystem() to initialize the KJ filesystem API. Usually you should do
  //   this at a high level in your program, e.g. the main() function, and then pass down the
  //   appropriate File/Directory objects to the components that need them. Example:
  //
  //     auto fs = kj::newDiskFilesystem();
  //     SchemaParser parser;
  //     auto schema = parser->parseFromDirectory(fs->getCurrent(),
  //         kj::Path::parse("foo/bar.capnp"), nullptr);
  //
  // Hint: To use in-memory data rather than real disk, you can use kj::newInMemoryDirectory(),
  //   write the files you want, then pass it to SchemaParser. Example:
  //
  //     auto dir = kj::newInMemoryDirectory(kj::nullClock());
  //     auto path = kj::Path::parse("foo/bar.capnp");
  //     dir->openFile(path, kj::WriteMode::CREATE | kj::WriteMode::CREATE_PARENT)
  //        ->writeAll("struct Foo {}");
  //     auto schema = parser->parseFromDirectory(*dir, path, nullptr);
  //
  // Hint: You can create an in-memory directory but then populate it with real files from disk,
  //   in order to control what is visible while also avoiding reading files yourself or making
  //   extra copies. Example:
  //
  //     auto fs = kj::newDiskFilesystem();
  //     auto dir = kj::newInMemoryDirectory(kj::nullClock());
  //     auto fakePath = kj::Path::parse("foo/bar.capnp");
  //     auto realPath = kj::Path::parse("path/to/some/file.capnp");
  //     dir->transfer(fakePath, kj::WriteMode::CREATE | kj::WriteMode::CREATE_PARENT,
  //                   fs->getCurrent(), realPath, kj::TransferMode::LINK);
  //     auto schema = parser->parseFromDirectory(*dir, fakePath, nullptr);
  //
  //   In this example, note that any imports in the file will fail, since the in-memory directory
  //   you created contains no files except the specific one you linked in.

  ParsedSchema parseDiskFile(kj::StringPtr displayName, kj::StringPtr diskPath,
                             kj::ArrayPtr<const kj::StringPtr> importPath) const
      CAPNP_DEPRECATED("Use parseFromDirectory() instead.");
  // Creates a private kj::Filesystem and uses it to parse files from the real disk.
  //
  // DO NOT USE in new code. Use parseFromDirectory() instead.
  //
  // This API has a serious problem: the file can import and embed files located anywhere on disk
  // using relative paths. Even if you specify no `importPath`, relative imports still work. By
  // using `parseFromDirectory()`, you can arrange so that imports are only allowed within a
  // particular directory, or even set up a dummy filesystem where other files are not visible.

  void setDiskFilesystem(kj::Filesystem& fs)
      CAPNP_DEPRECATED("Use parseFromDirectory() instead.");
  // Call before calling parseDiskFile() to choose an alternative disk filesystem implementation.
  // This exists mostly for testing purposes; new code should use parseFromDirectory() instead.
  //
  // If parseDiskFile() is called without having called setDiskFilesystem(), then
  // kj::newDiskFilesystem() will be used instead.
122

123
  ParsedSchema parseFile(kj::Own<SchemaFile>&& file) const;
124
  // Advanced interface for parsing a file that may or may not be located in any global namespace.
125
  // Most users will prefer `parseFromDirectory()`.
126 127 128 129 130 131 132 133 134
  //
  // If the file has already been parsed (that is, a SchemaFile that compares equal to this one
  // was parsed previously), the existing schema will be returned again.
  //
  // This method reports errors by calling SchemaFile::reportError() on the file where the error
  // is located.  If that call does not throw an exception, `parseFile()` may in fact return
  // normally.  In this case, the result is a best-effort attempt to compile the schema, but it
  // may be invalid or corrupt, and using it for anything may cause exceptions to be thrown.

135 136 137 138 139
  kj::Maybe<schema::Node::SourceInfo::Reader> getSourceInfo(Schema schema) const;
  // Look up source info (e.g. doc comments) for the given schema, which must have come from this
  // SchemaParser. Note that this will also work for implicit group and param types that don't have
  // a type name hence don't have a `ParsedSchema`.

140 141 142 143 144 145
  template <typename T>
  inline void loadCompiledTypeAndDependencies() {
    // See SchemaLoader::loadCompiledTypeAndDependencies().
    getLoader().loadCompiledTypeAndDependencies<T>();
  }

146 147
private:
  struct Impl;
148
  struct DiskFileCompat;
149 150 151 152
  class ModuleImpl;
  kj::Own<Impl> impl;
  mutable bool hadErrors = false;

153
  ModuleImpl& getModuleImpl(kj::Own<SchemaFile>&& file) const;
154
  SchemaLoader& getLoader();
155 156 157 158 159 160 161 162 163 164 165

  friend class ParsedSchema;
};

class ParsedSchema: public Schema {
  // ParsedSchema is an extension of Schema which also has the ability to look up nested nodes
  // by name.  See `SchemaParser`.

public:
  inline ParsedSchema(): parser(nullptr) {}

166
  kj::Maybe<ParsedSchema> findNested(kj::StringPtr name) const;
167 168 169
  // Gets the nested node with the given name, or returns null if there is no such nested
  // declaration.

170
  ParsedSchema getNested(kj::StringPtr name) const;
171 172 173
  // Gets the nested node with the given name, or throws an exception if there is no such nested
  // declaration.

174 175 176
  schema::Node::SourceInfo::Reader getSourceInfo() const;
  // Get the source info for this schema.

177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193
private:
  inline ParsedSchema(Schema inner, const SchemaParser& parser): Schema(inner), parser(&parser) {}

  const SchemaParser* parser;
  friend class SchemaParser;
};

// =======================================================================================
// Advanced API

class SchemaFile {
  // Abstract interface representing a schema file.  You can implement this yourself in order to
  // gain more control over how the compiler resolves imports and reads files.  For the
  // common case of files on disk or other global filesystem-like namespaces, use
  // `SchemaFile::newDiskFile()`.

public:
194 195 196 197 198 199
  // Note: Cap'n Proto 0.6.x and below had classes FileReader and DiskFileReader and a method
  //   newDiskFile() defined here. These were removed when SchemaParser was transitioned to use the
  //   KJ filesystem API. You should be able to get the same effect by subclassing
  //   kj::ReadableDirectory, or using kj::newInMemoryDirectory().

  static kj::Own<SchemaFile> newFromDirectory(
200 201
      const kj::ReadableDirectory& baseDir, kj::Path path,
      kj::ArrayPtr<const kj::ReadableDirectory* const> importPath,
202 203 204 205 206 207
      kj::Maybe<kj::String> displayNameOverride = nullptr);
  // Construct a SchemaFile representing a file in a kj::ReadableDirectory. This is used to
  // implement SchemaParser::parseFromDirectory(); see there for details.
  //
  // The SchemaFile compares equal to any other SchemaFile that has exactly the same `baseDir`
  // object (by identity) and `path` (by value).
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

  // -----------------------------------------------------------------
  // For more control, you can implement this interface.

  virtual kj::StringPtr getDisplayName() const = 0;
  // Get the file's name, as it should appear in the schema.

  virtual kj::Array<const char> readContent() const = 0;
  // Read the file's entire content and return it as a byte array.

  virtual kj::Maybe<kj::Own<SchemaFile>> import(kj::StringPtr path) const = 0;
  // Resolve an import, relative to this file.
  //
  // `path` is exactly what appears between quotes after the `import` keyword in the source code.
  // It is entirely up to the `SchemaFile` to decide how to map this to another file.  Typically,
  // a leading '/' means that the file is an "absolute" path and is searched for in some list of
  // schema file repositories.  On the other hand, a path that doesn't start with '/' is relative
  // to the importing file.

  virtual bool operator==(const SchemaFile& other) const = 0;
  virtual bool operator!=(const SchemaFile& other) const = 0;
  virtual size_t hashCode() const = 0;
  // Compare two SchemaFiles to see if they refer to the same underlying file.  This is an
  // optimization used to avoid the need to re-parse a file to check its ID.

  struct SourcePos {
    uint byte;
    uint line;
    uint column;
  };
  virtual void reportError(SourcePos start, SourcePos end, kj::StringPtr message) const = 0;
  // Report that the file contains an error at the given interval.

private:
  class DiskSchemaFile;
};

}  // namespace capnp