kea_docgen.cc 16.3 KB
Newer Older
1
2
3
4
5
6
// Copyright (C) 2018 Internet Systems Consortium, Inc. ("ISC")
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

7
8
9
10
11
#include <iostream>
#include <fstream>
#include <sstream>
#include <vector>
#include <map>
12
#include <set>
13
#include <ctime>
14
15
16
17
18
19
20
21
22

#include <exceptions/exceptions.h>
#include <cc/data.h>

using namespace std;
using namespace isc;
using namespace isc::data;


23
/// @brief API documentation generator
24
25
26
class DocGen {
public:

27
    /// Output location of a file.
28
29
    const string OUTPUT = "guide/api.xml";

30
    /// Controls whether to print out extra information.
31
    bool verbose = false;
32

33
34
35
    /// @brief Load JSON files that each contain description of an API command
    ///
    /// @param files a vector with names of files.
36
37
38
39
40
41
    void loadFiles(const vector<string>& files) {

        map <string, ElementPtr> commands;

        int cnt = 0;

Tomek Mrugalski's avatar
Tomek Mrugalski committed
42
43
        int errors = 0; // number of errors encountered

44
45
        try {
            for (auto f : files) {
46
47
48
49
50
51
52
53
54
55
56
57
                string cmd = f;
                size_t pos = f.find_last_of('/');
                if (pos != string::npos) {
                    cmd = f.substr(pos + 1, -1);
                }
                cmd = cmd.substr(0, cmd.find("."));

                if (cmd == "_template") {
                    cout << "Skipping template file (_template.json)" << endl;
                    continue;
                }

Tomek Mrugalski's avatar
Tomek Mrugalski committed
58
59
60
61
62
63
                try {
                    cout << "Loading description of command " << cmd << "... ";
                    ElementPtr x = Element::fromJSONFile(f, false);
                    cout << "loaded, sanity check...";

                    sanityCheck(f, x);
64

Tomek Mrugalski's avatar
Tomek Mrugalski committed
65
66
                    cmds_.insert(make_pair(cmd, x));
                    cout << " looks ok." << endl;
67

Tomek Mrugalski's avatar
Tomek Mrugalski committed
68
69
70
71
72
73
74
75
                } catch (const exception& e) {
                    cout << "ERROR: " << e.what() << endl;
                    errors++;
                }

                if (errors) {
                    continue;
                }
76
77
78
79
80
81
82
83
84

                cnt++;
            }
        } catch (const Unexpected& e) {
            isc_throw(Unexpected, e.what() << " while processing "
                      << cnt + 1 << " file out of " << files.size());
        }

        cout << "Loaded " << cmds_.size() << " commands out of " << files.size()
Tomek Mrugalski's avatar
Tomek Mrugalski committed
85
86
87
88
             << " file(s), " << errors << " error(s) detected." << endl;
        if (errors) {
            isc_throw(Unexpected, errors << " error(s) detected while loading JSON files");
        }
89
90
    }

91
92
93
94
95
96
    /// @brief checks if mandatory string parameter is specified
    ///
    /// @param x a map that is being checked
    /// @param name name of the string element expected to be there
    /// @param fname name of the file (used in error reporting)
    /// @throw Unexpected if missing, different type or empty
97
98
99
100
101
102
    void requireString(const ElementPtr& x, const string& name, const string& fname) {
        if (!x->contains(name)) {
            isc_throw(Unexpected, "Mandatory '" + name + " field missing while "
                      "processing file " + fname);
        }
        if (x->get(name)->getType() != Element::string) {
103
            isc_throw(Unexpected, "'" + name + " field is present, but is not a string"
104
105
106
                      " in file " + fname);
        }
        if (x->get(name)->stringValue().empty()) {
107
            isc_throw(Unexpected, "'" + name + " field is present, is a string, but is "
108
109
110
                      "empty in file " + fname);
        }
    }
111

112
113
114
115
116
117
    /// @brief checks if mandatory list parameter is specified
    ///
    /// @param x a map that is being checked
    /// @param name name of the list element expected to be there
    /// @param fname name of the file (used in error reporting)
    /// @throw Unexpected if missing, different type or empty
118
119
120
121
122
123
    void requireList(const ElementPtr& x, const string& name, const string& fname) {
        if (!x->contains(name)) {
            isc_throw(Unexpected, "Mandatory '" + name + " field missing while "
                      "processing file " + fname);
        }
        if (x->get(name)->getType() != Element::list) {
124
            isc_throw(Unexpected, "'" + name + " field is present, but is not a list "
125
126
                      "in file " + fname);
        }
127

128
        ConstElementPtr l = x->get(name);
129

130
        if (l->size() == 0) {
131
            isc_throw(Unexpected, "'" + name + " field is a list, but is empty in file "
132
133
                      + fname);
        }
134

135
136
        // todo: check that every element is a string
    }
137

138
139
140
141
    /// @brief Checks that the essential parameters for each command are defined
    ///
    /// @param fname name of the file the data was read from (printed if error is detected)
    /// @param x a JSON map that contains content of the file
142
143
144
145
146
147
    void sanityCheck(const string& fname, const ElementPtr& x) {
        requireString(x, "name", fname);
        requireString(x, "brief", fname);
        requireList  (x, "support", fname);
        requireString(x, "avail", fname);
        requireString(x, "brief", fname);
148
149

        // They're optional.
150
        //requireString(x, "cmd-syntax", fname);
151
152
153
        //requireString(x, "cmd-comment", fname);
        //requireString(x, "resp-syntax", fname);
        //requireString(x, "resp-comment", fname);
154
155
    }

156
157
158
    /// @brief Writes ISC copyright note to the stream
    ///
    /// @param f stream to write copyrights to
159
160
    void generateCopyright(stringstream& f) {
        f << "<!--" << endl;
161
162
163
164
165
166
167
168
169
170

        std::time_t t = time(0);
        std::tm* now = std::localtime(&t);

        if (now->tm_year + 1900 == 2018) {
            f << " - Copyright (C) 2018 Internet Systems Consortium, Inc. (\"ISC\")" << endl;
        } else {
            // Whoaa! the future is now!
            f << " - Copyright (C) 2018-" << (now->tm_year + 1900) << " Internet Systems Consortium, Inc. (\"ISC\")" << endl;
        }
171
172
173
174
175
176
177
178
        f << " -" << endl;
        f << " - This Source Code Form is subject to the terms of the Mozilla Public" << endl;
        f << " - License, v. 2.0. If a copy of the MPL was not distributed with this" << endl;
        f << " - file, You can obtain one at http://mozilla.org/MPL/2.0/." << endl;
        f << " -->" << endl;
        f << endl;
        f << "<!-- autogenerated using cmd_docgen. Do not edit by hand! -->" << endl;
    }
179

180
181
182
183
    /// @brief generates a link to command
    ///
    /// @param f stream to write the generated link to
    /// @param cmd name of the command
184
    void generateCmdLink(stringstream& f, const string& cmd) {
185
186
        f << "<command><link linkend=\"ref-" << cmd << "\">" << cmd
          << "</link></command>" << endl;
187
    }
188

189
190
191
192
193
194
195
196
    /// @brief generates lists of all commands.
    ///
    /// Currently there are several lists (or rather lists of lists). They all enumerate
    /// commands, but each list serving a different purpose:
    /// - list of commands supported by a daemon
    /// - list of commands provided by a hook
    ///
    /// @param f stream to write the generated lists to
197
198
    void generateLists(stringstream& f) {
        // Generate a list of all commands
199
        f << "  <para>Kea currently supports " << cmds_.size() << " commands:" << endl;
200

201
        bool first = true;
202
        for (auto cmd : cmds_) {
203
204
205
            if (!first) {
                f << ", ";
            }
206
            generateCmdLink(f, cmd.first);
207

208
            first = false;
209
        }
210

211
        f << ".</para>" << endl;
212

213
214
        // Generate a list of components:
        set<string> all_daemons;
215
        set<string> all_hooks;
216
217
        for (auto cmd : cmds_) {
            auto daemons = cmd.second->get("support");
218
            auto hook = cmd.second->get("hook");
219
220
221
222
223
224
            for (int i = 0; i < daemons->size(); i++) {
                string daemon = daemons->get(i)->stringValue();
                if (all_daemons.find(daemon) == all_daemons.end()) {
                    all_daemons.insert(daemon);
                }
            }
225
226
227
228
229
230
            if (hook) {
                string hook_txt = hook->stringValue();
                if (all_hooks.find(hook_txt) == all_hooks.end()) {
                    all_hooks.insert(hook_txt);
                }
            }
231
232
        }

233
234
        cout << all_daemons.size() << " daemon(s) detected." << endl;
        cout << all_hooks.size() << " hook lib(s) detected." << endl;
235
236
237

        for (auto daemon : all_daemons) {
            f << "<para xml:id=\"commands-" << daemon << "\">"
238
              << "Commands supported by " << daemon << " daemon: ";
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258

            bool first = true;
            for (auto cmd : cmds_) {

                auto daemons = cmd.second->get("support");
                for (auto d : daemons->listValue()) {
                    if (d->stringValue() == daemon) {
                        if (!first) {
                            f << ", ";
                        }
                        generateCmdLink(f, cmd.first);
                        first = false;
                        break; // get to next command
                    }
                }
            }

            f << ".</para>" << endl;
        }

259

260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
        for (auto hook : all_hooks) {
            f << "<para xml:id=\"commands-" << hook << "-lib\">"
              << "Commands supported by " << hook << " hook library: ";

            bool first = true;
            for (auto cmd : cmds_) {

                auto daemon_hook = cmd.second->get("hook");
                if (!daemon_hook || daemon_hook->stringValue() != hook) {
                    continue;
                }
                if (!first) {
                    f << ", ";
                }
                generateCmdLink(f, cmd.first);
                first = false;
            }

            f << ".</para>" << endl;
        }

281
282
    }

283
    /// @brief generates the whole API documentation
284
285
286
287
288
289
    void generateOutput() {

        stringstream f;

        generateCopyright(f);

290
        f << "<appendix xmlns=\"http://docbook.org/ns/docbook\" version=\"5.0\" xml:id=\"api\">"
291
292
293
294
295
296
          << endl;
        f << "  <title>API Reference</title>" << endl;


        generateLists(f);

297
298
        // Generate actual commands references.
        generateCommands(f);
299

300
        f << "</appendix>" << endl;
301

302
303
        ofstream file(OUTPUT.c_str(), ofstream::trunc);
        file << f.str();
304
305
306
307
308
309
310
311
        if (verbose) {
            cout << "----------------" << endl;
            cout << f.str();
            cout << "----------------" << endl;
        }
        file.close();

        cout << "Output written to " << OUTPUT << endl;
312
    }
313

314
315
316
    /// @brief generate sections for all commands
    ///
    /// @param f stream to write the commands to
317
    void generateCommands(stringstream& f){
318

319
320
321
322
323
324
325
326
327
328
329
        for (auto cmd : cmds_) {
            f << "<!-- start of " << cmd.first << " -->" << endl;
            f << "<section xml:id=\"reference-" << cmd.first << "\">" << endl;
            f << "<title>" << cmd.first << " reference</title>" << endl;
            generateCommand(f, cmd.second);
            f << "</section>" << endl;
            f << "<!-- end of " << cmd.first << " -->" << endl;
            f << endl;
        }
    }

330
331
332
333
334
335
336
337
338
339
340
341
342
    /// @brief replace all strings
    ///
    /// @param str [in,out] this string will have some replacements
    /// @param from what to replace
    /// @param to what to replace with
    void replaceAll(std::string& str, const std::string& from, const std::string& to) {
        if(from.empty())
            return;
        size_t start_pos = 0;
        while((start_pos = str.find(from, start_pos)) != std::string::npos) {
            str.replace(start_pos, from.length(), to);
            start_pos += to.length();
        }
343
344
    }

345
346
347
348
349
    /// @brief escapes string to be safe for XML (docbook)
    ///
    /// @param txt string to be escaped
    /// @return escaped string
    string escapeString(string txt) {
350

351
352
353
354
        replaceAll(txt, "<", "&lt;");
        replaceAll(txt, ">", "&gt;");
        return (txt);
    }
355

356
357
358
359
360
361
362
363
364
365
366
367
368
    /// @brief generates standard description of command's response
    ///
    /// If a command doesn't have response syntax specified, we will
    /// assume it follows the usual syntax and provide the default description.
    string standardResponseSyntax() {
        stringstream t;

        t << "{" << endl
          << "    \"result\": <integer>," << endl
          << "    \"text\": <string>" << endl
          << "}" << endl;
        return (t.str());
    }
369

370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
    /// @brief generates standard description of command's comment
    ///
    /// If a command doesn't have response syntax comment specified, we will
    /// assume it follows the usual syntax and provide the default description.
    string standardResponseComment() {
        stringstream t;

        t << "Result is an integer representation of the status. Currently supported"
          << " statuses are:" << endl
          << "<itemizedlist>" << endl
          << "  <listitem><para>0 - success</para></listitem>" << endl
          << "  <listitem><para>1 - error</para></listitem>" << endl
          << "  <listitem><para>2 - unsupported</para></listitem>" << endl
          << "  <listitem><para>3 - empty (command was completed successfully, but "
          <<                        "no data was affected or returned)</para>"
          <<                        "</listitem>" << endl
          << "</itemizedlist>" << endl;
        return (t.str());
    }
389

390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
    /// @brief generates command description
    ///
    /// @param f stream to write the description to
    /// @param cmd pointer to JSON structure that describes the command
    void generateCommand(stringstream& f, const ElementPtr& cmd) {

        // command overview
        f << "<para xml:id=\"ref-" << cmd->get("name")->stringValue() << "\"><command>"
          << cmd->get("name")->stringValue() << "</command> - "
          << cmd->get("brief")->stringValue() << "</para>" << endl << endl;

        // command can be issued to the following daemons
        f << "<para>Supported by: ";
        ConstElementPtr daemons = cmd->get("support");
        for (int i = 0; i < daemons->size(); i++) {
            if (i) {
                f << ", ";
            }
408

409
410
411
412
413
414
415
416
417
418
            f << "<command><link linkend=\"commands-" << daemons->get(i)->stringValue()
              << "\">" << daemons->get(i)->stringValue() << "</link></command>";
        }
        f << "</para>" << endl << endl;

        // availability
        f << "<para>Availability: " << cmd->get("avail")->stringValue();
        auto hook = cmd->get("hook");
        if (hook) {
            f << " (<link linkend=\"commands-" << hook->stringValue() << "-lib\">"
419
              << hook->stringValue() << "</link>  hook)";
420
421
        } else {
            f << " (built-in)";
422
423
        }

424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
        f << "</para>" << endl << endl;

        // description and examples
        f << "<para>Description and examples: See <xref linkend=\"command-"
          << cmd->get("name")->stringValue() << "\"/></para>" << endl << endl;

        // Command syntax:
        f << "<para>Command syntax:" << endl;
        if (cmd->contains("cmd-syntax")) {
            f << "  <screen>" << escapeString(cmd->get("cmd-syntax")->stringValue())
              << "</screen>" << endl;
        } else {
            f << "  <screen>{" << endl
              << "    \"command\": \"" << cmd->get("name")->stringValue() << "\"" << endl
              << "}</screen>" << endl;
        }
        if (cmd->contains("cmd-comment")) {
441
            f << cmd->get("cmd-comment")->stringValue();
442
443
        }
        f << "</para>" << endl << endl;
444

445
446
447
        // Response syntax
        f << "<para>Response syntax:" << endl
          << "  <screen>";
448

449
450
451
452
453
454
        if (cmd->contains("resp-syntax")) {
            f << escapeString(cmd->get("resp-syntax")->stringValue());
        } else {
            f << escapeString(standardResponseSyntax());
        }
        f << "</screen>" << endl;
455

456
457
458
459
460
461
        if (cmd->contains("resp-comment")) {
            f << cmd->get("resp-comment")->stringValue();
        } else {
            f << standardResponseComment();
        }
        f << "</para>" << endl << endl;
462
    }
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488

    map<string, ElementPtr> cmds_;
};

int main(int argc, const char*argv[]) {

    vector<string> files;

    for (int i = 1; i < argc; i++) {
        files.push_back(string(argv[i]));
    }

    cout << "Loading " << files.size() << " files(s)." << endl;

    try {
        DocGen doc_gen;

        doc_gen.loadFiles(files);

        doc_gen.generateOutput();
    } catch (const exception& e) {
        cerr << "ERROR: " << e.what() << endl;
    }

    return (0);
}