1// Copyright (c) 2013-2014 Sandstorm Development Group, Inc. and contributors
2// Licensed under the MIT License:
3//
4// Permission is hereby granted, free of charge, to any person obtaining a copy
5// of this software and associated documentation files (the "Software"), to deal
6// in the Software without restriction, including without limitation the rights
7// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8// copies of the Software, and to permit persons to whom the Software is
9// furnished to do so, subject to the following conditions:
10//
11// The above copyright notice and this permission notice shall be included in
12// all copies or substantial portions of the Software.
13//
14// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20// THE SOFTWARE.
21
22#pragma once
23
24#if defined(__GNUC__) && !defined(CAPNP_HEADER_WARNINGS)
25#pragma GCC system_header
26#endif
27
28#include "schema-loader.h"
29#include <kj/string.h>
30#include <kj/filesystem.h>
31
32namespace capnp {
33
34class ParsedSchema;
35class SchemaFile;
36
37class SchemaParser {
38 // Parses `.capnp` files to produce `Schema` objects.
39 //
40 // This class is thread-safe, hence all its methods are const.
41
42public:
43 SchemaParser();
44 ~SchemaParser() noexcept(false);
45
46 ParsedSchema parseFromDirectory(
47 const kj::ReadableDirectory& baseDir, kj::Path path,
48 kj::ArrayPtr<const kj::ReadableDirectory* const> importPath) const;
49 // Parse a file from the KJ filesystem API. Throws an exception if the file dosen't exist.
50 //
51 // `baseDir` and `path` are used together to resolve relative imports. `path` is the source
52 // file's path within `baseDir`. Relative imports will be interpreted relative to `path` and
53 // will be opened using `baseDir`. Note that the KJ filesystem API prohibits "breaking out" of
54 // a directory using "..", so relative imports will be restricted to children of `baseDir`.
55 //
56 // `importPath` is used for absolute imports (imports that start with a '/'). Each directory in
57 // the array will be searched in order until a file is found.
58 //
59 // All `ReadableDirectory` objects must remain valid until the `SchemaParser` is destroyed. Also,
60 // 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 // This method throws an exception if any errors are encountered in the file or in anything the
66 // file depends on. Note that merely importing another file does not count as a dependency on
67 // anything in the imported file -- only the imported types which are actually used are
68 // "dependencies".
69 //
70 // Hint: Use kj::newDiskFilesystem() to initialize the KJ filesystem API. Usually you should do
71 // this at a high level in your program, e.g. the main() function, and then pass down the
72 // appropriate File/Directory objects to the components that need them. Example:
73 //
74 // auto fs = kj::newDiskFilesystem();
75 // SchemaParser parser;
76 // auto schema = parser->parseFromDirectory(fs->getCurrent(),
77 // kj::Path::parse("foo/bar.capnp"), nullptr);
78 //
79 // Hint: To use in-memory data rather than real disk, you can use kj::newInMemoryDirectory(),
80 // write the files you want, then pass it to SchemaParser. Example:
81 //
82 // auto dir = kj::newInMemoryDirectory(kj::nullClock());
83 // auto path = kj::Path::parse("foo/bar.capnp");
84 // dir->openFile(path, kj::WriteMode::CREATE | kj::WriteMode::CREATE_PARENT)
85 // ->writeAll("struct Foo {}");
86 // auto schema = parser->parseFromDirectory(*dir, path, nullptr);
87 //
88 // Hint: You can create an in-memory directory but then populate it with real files from disk,
89 // in order to control what is visible while also avoiding reading files yourself or making
90 // extra copies. Example:
91 //
92 // auto fs = kj::newDiskFilesystem();
93 // auto dir = kj::newInMemoryDirectory(kj::nullClock());
94 // auto fakePath = kj::Path::parse("foo/bar.capnp");
95 // auto realPath = kj::Path::parse("path/to/some/file.capnp");
96 // dir->transfer(fakePath, kj::WriteMode::CREATE | kj::WriteMode::CREATE_PARENT,
97 // fs->getCurrent(), realPath, kj::TransferMode::LINK);
98 // auto schema = parser->parseFromDirectory(*dir, fakePath, nullptr);
99 //
100 // In this example, note that any imports in the file will fail, since the in-memory directory
101 // you created contains no files except the specific one you linked in.
102
103 ParsedSchema parseDiskFile(kj::StringPtr displayName, kj::StringPtr diskPath,
104 kj::ArrayPtr<const kj::StringPtr> importPath) const
105 CAPNP_DEPRECATED("Use parseFromDirectory() instead.");
106 // Creates a private kj::Filesystem and uses it to parse files from the real disk.
107 //
108 // DO NOT USE in new code. Use parseFromDirectory() instead.
109 //
110 // This API has a serious problem: the file can import and embed files located anywhere on disk
111 // using relative paths. Even if you specify no `importPath`, relative imports still work. By
112 // using `parseFromDirectory()`, you can arrange so that imports are only allowed within a
113 // particular directory, or even set up a dummy filesystem where other files are not visible.
114
115 void setDiskFilesystem(kj::Filesystem& fs)
116 CAPNP_DEPRECATED("Use parseFromDirectory() instead.");
117 // Call before calling parseDiskFile() to choose an alternative disk filesystem implementation.
118 // This exists mostly for testing purposes; new code should use parseFromDirectory() instead.
119 //
120 // If parseDiskFile() is called without having called setDiskFilesystem(), then
121 // 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 // If the file has already been parsed (that is, a SchemaFile that compares equal to this one
128 // was parsed previously), the existing schema will be returned again.
129 //
130 // This method reports errors by calling SchemaFile::reportError() on the file where the error
131 // is located. If that call does not throw an exception, `parseFile()` may in fact return
132 // normally. In this case, the result is a best-effort attempt to compile the schema, but it
133 // may be invalid or corrupt, and using it for anything may cause exceptions to be thrown.
134
135 kj::Maybe<schema::Node::SourceInfo::Reader> getSourceInfo(Schema schema) const;
136 // Look up source info (e.g. doc comments) for the given schema, which must have come from this
137 // SchemaParser. Note that this will also work for implicit group and param types that don't have
138 // a type name hence don't have a `ParsedSchema`.
139
140 template <typename T>
141 inline void loadCompiledTypeAndDependencies() {
142 // See SchemaLoader::loadCompiledTypeAndDependencies().
143 getLoader().loadCompiledTypeAndDependencies<T>();
144 }
145
146private:
147 struct Impl;
148 struct DiskFileCompat;
149 class ModuleImpl;
150 kj::Own<Impl> impl;
151 mutable bool hadErrors = false;
152
153 ModuleImpl& getModuleImpl(kj::Own<SchemaFile>&& file) const;
154 SchemaLoader& getLoader();
155
156 friend class ParsedSchema;
157};
158
159class ParsedSchema: public Schema {
160 // ParsedSchema is an extension of Schema which also has the ability to look up nested nodes
161 // by name. See `SchemaParser`.
162
163public:
164 inline ParsedSchema(): parser(nullptr) {}
165
166 kj::Maybe<ParsedSchema> findNested(kj::StringPtr name) const;
167 // Gets the nested node with the given name, or returns null if there is no such nested
168 // declaration.
169
170 ParsedSchema getNested(kj::StringPtr name) const;
171 // Gets the nested node with the given name, or throws an exception if there is no such nested
172 // declaration.
173
174 schema::Node::SourceInfo::Reader getSourceInfo() const;
175 // Get the source info for this schema.
176
177private:
178 inline ParsedSchema(Schema inner, const SchemaParser& parser): Schema(inner), parser(&parser) {}
179
180 const SchemaParser* parser;
181 friend class SchemaParser;
182};
183
184// =======================================================================================
185// Advanced API
186
187class SchemaFile {
188 // Abstract interface representing a schema file. You can implement this yourself in order to
189 // gain more control over how the compiler resolves imports and reads files. For the
190 // common case of files on disk or other global filesystem-like namespaces, use
191 // `SchemaFile::newDiskFile()`.
192
193public:
194 // Note: Cap'n Proto 0.6.x and below had classes FileReader and DiskFileReader and a method
195 // newDiskFile() defined here. These were removed when SchemaParser was transitioned to use the
196 // KJ filesystem API. You should be able to get the same effect by subclassing
197 // kj::ReadableDirectory, or using kj::newInMemoryDirectory().
198
199 static kj::Own<SchemaFile> newFromDirectory(
200 const kj::ReadableDirectory& baseDir, kj::Path path,
201 kj::ArrayPtr<const kj::ReadableDirectory* const> importPath,
202 kj::Maybe<kj::String> displayNameOverride = nullptr);
203 // Construct a SchemaFile representing a file in a kj::ReadableDirectory. This is used to
204 // implement SchemaParser::parseFromDirectory(); see there for details.
205 //
206 // The SchemaFile compares equal to any other SchemaFile that has exactly the same `baseDir`
207 // object (by identity) and `path` (by value).
208
209 // -----------------------------------------------------------------
210 // For more control, you can implement this interface.
211
212 virtual kj::StringPtr getDisplayName() const = 0;
213 // Get the file's name, as it should appear in the schema.
214
215 virtual kj::Array<const char> readContent() const = 0;
216 // Read the file's entire content and return it as a byte array.
217
218 virtual kj::Maybe<kj::Own<SchemaFile>> import(kj::StringPtr path) const = 0;
219 // Resolve an import, relative to this file.
220 //
221 // `path` is exactly what appears between quotes after the `import` keyword in the source code.
222 // It is entirely up to the `SchemaFile` to decide how to map this to another file. Typically,
223 // a leading '/' means that the file is an "absolute" path and is searched for in some list of
224 // schema file repositories. On the other hand, a path that doesn't start with '/' is relative
225 // to the importing file.
226
227 virtual bool operator==(const SchemaFile& other) const = 0;
228 virtual bool operator!=(const SchemaFile& other) const = 0;
229 virtual size_t hashCode() const = 0;
230 // Compare two SchemaFiles to see if they refer to the same underlying file. This is an
231 // optimization used to avoid the need to re-parse a file to check its ID.
232
233 struct SourcePos {
234 uint byte;
235 uint line;
236 uint column;
237 };
238 virtual void reportError(SourcePos start, SourcePos end, kj::StringPtr message) const = 0;
239 // Report that the file contains an error at the given interval.
240
241private:
242 class DiskSchemaFile;
243};
244
245} // namespace capnp
246