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 | |
32 | namespace capnp { |
33 | |
34 | class ParsedSchema; |
35 | class SchemaFile; |
36 | |
37 | class SchemaParser { |
38 | // Parses `.capnp` files to produce `Schema` objects. |
39 | // |
40 | // This class is thread-safe, hence all its methods are const. |
41 | |
42 | public: |
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 | |
146 | private: |
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 | |
159 | class 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 | |
163 | public: |
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 | |
177 | private: |
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 | |
187 | class 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 | |
193 | public: |
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 | |
241 | private: |
242 | class DiskSchemaFile; |
243 | }; |
244 | |
245 | } // namespace capnp |
246 | |