diff options
-rw-r--r-- | mod/mod-ci-github.cxx | 1049 |
1 files changed, 530 insertions, 519 deletions
diff --git a/mod/mod-ci-github.cxx b/mod/mod-ci-github.cxx index 7497835..de7d792 100644 --- a/mod/mod-ci-github.cxx +++ b/mod/mod-ci-github.cxx @@ -81,668 +81,679 @@ using namespace butl; using namespace web; using namespace brep::cli; -// @@ Let's move everything to the brep namespace and get rid of -// explicit brep:: qualifications. - -brep::ci_github:: -ci_github (const ci_github& r) - : handler (r), - options_ (r.initialized_ ? r.options_ : nullptr) +namespace brep { -} + // GitHub-specific types. + // + // @@ TMP A brep::repository here would clash with the pre-existing + // brep::repository. + // + namespace gh + { + // The "check_suite" object within a check_quite webhook request. + // + struct check_suite + { + uint64_t id; + string head_branch; + string head_sha; + string before; + string after; -void brep::ci_github:: -init (scanner& s) -{ - options_ = make_shared<options::ci_github> ( - s, unknown_mode::fail, unknown_mode::fail); -} + explicit + check_suite (json::parser&); -// The "check_suite" object within a check_quite webhook request. -// -struct check_suite -{ - uint64_t id; - string head_branch; - string head_sha; - string before; - string after; + check_suite () = default; + }; + + struct repository + { + string name; + string full_name; + string default_branch; - explicit - check_suite (json::parser&); + explicit + repository (json::parser&); - check_suite () = default; -}; + repository () = default; + }; -struct repository -{ - string name; - string full_name; - string default_branch; + struct installation + { + uint64_t id; - explicit - repository (json::parser&); + explicit + installation (json::parser&); - repository () = default; -}; + installation () = default; + }; -struct installation -{ - uint64_t id; + struct check_suite_event + { + string action; + gh::check_suite check_suite; + gh::repository repository; + gh::installation installation; - explicit - installation (json::parser&); + explicit + check_suite_event (json::parser&); - installation () = default; -}; + check_suite_event () = default; + }; -struct check_suite_event -{ - string action; - ::check_suite check_suite; - ::repository repository; - ::installation installation; + struct installation_access_token + { + string token; + timestamp expires_at; - explicit - check_suite_event (json::parser&); + explicit + installation_access_token (json::parser&); - check_suite_event () = default; -}; + installation_access_token () = default; + }; -struct installation_access_token -{ - string token; - timestamp expires_at; + static ostream& + operator<< (ostream&, const check_suite&); - explicit - installation_access_token (json::parser&); + static ostream& + operator<< (ostream&, const repository&); - installation_access_token () = default; -}; + static ostream& + operator<< (ostream&, const installation&); -static ostream& -operator<< (ostream&, const check_suite&); + static ostream& + operator<< (ostream&, const check_suite_event&); -static ostream& -operator<< (ostream&, const repository&); + static ostream& + operator<< (ostream&, const installation_access_token&); + } -static ostream& -operator<< (ostream&, const installation&); + using namespace gh; -static ostream& -operator<< (ostream&, const check_suite_event&); + ci_github:: + ci_github (const ci_github& r) + : handler (r), + options_ (r.initialized_ ? r.options_ : nullptr) + { + } -static ostream& -operator<< (ostream&, const installation_access_token&); + void ci_github:: + init (scanner& s) + { + options_ = make_shared<options::ci_github> ( + s, unknown_mode::fail, unknown_mode::fail); + } -// Read the HTTP response status code from an input stream. -// -// Parse the status code from the HTTP status line, skip over the remaining -// headers (leaving the stream at the beginning of the response body), and -// return the status code. -// -// Throw system_error(EINVAL) if the status line could not be parsed. -// -// Note that this implementation is almost identical to that of bpkg's -// start_curl() function in fetch.cxx. -// -static uint16_t -read_status_code (ifdstream& in) -{ - // After getting the status code we will read until the empty line - // (containing just CRLF). Not being able to reach such a line is an error, - // which is the reason for the exception mask choice. When done, we will - // restore the original exception mask. + // Read the HTTP response status code from an input stream. // - // @@ TMP Presumably curl would already have failed if the server's - // response was malformed, right? So if we get here the only way to - // get EOF would be an I/O error? + // Parse the status code from the HTTP status line, skip over the remaining + // headers (leaving the stream at the beginning of the response body), and + // return the status code. // - ifdstream::iostate es (in.exceptions ()); - - in.exceptions ( - ifdstream::badbit | ifdstream::failbit | ifdstream::eofbit); - - // Parse and return the HTTP status code. Return 0 if the argument is - // invalid. + // Throw system_error(EINVAL) if the status line could not be parsed. // - auto status_code = [] (const string& s) + // Note that this implementation is almost identical to that of bpkg's + // start_curl() function in fetch.cxx. + // + static uint16_t + read_status_code (ifdstream& in) { - char* e (nullptr); - unsigned long c (strtoul (s.c_str (), &e, 10)); // Can't throw. - assert (e != nullptr); + // After getting the status code we will read until the empty line + // (containing just CRLF). Not being able to reach such a line is an error, + // which is the reason for the exception mask choice. When done, we will + // restore the original exception mask. + // + // @@ TMP Presumably curl would already have failed if the server's + // response was malformed, right? So if we get here the only way to + // get EOF would be an I/O error? + // + ifdstream::iostate es (in.exceptions ()); - return *e == '\0' && c >= 100 && c < 600 - ? static_cast<uint16_t> (c) - : 0; - }; + in.exceptions ( + ifdstream::badbit | ifdstream::failbit | ifdstream::eofbit); - // Read the CRLF-terminated line from the stream stripping the trailing - // CRLF. - // - auto read_line = [&in] () - { - string l; - getline (in, l); // Strips the trailing LF (0xA). + // Parse and return the HTTP status code. Return 0 if the argument is + // invalid. + // + auto status_code = [] (const string& s) + { + char* e (nullptr); + unsigned long c (strtoul (s.c_str (), &e, 10)); // Can't throw. + assert (e != nullptr); + + return *e == '\0' && c >= 100 && c < 600 + ? static_cast<uint16_t> (c) + : 0; + }; - // Note that on POSIX CRLF is not automatically translated into LF, so - // we need to strip CR (0xD) manually. + // Read the CRLF-terminated line from the stream stripping the trailing + // CRLF. // - if (!l.empty () && l.back () == '\r') - l.pop_back (); + auto read_line = [&in] () + { + string l; + getline (in, l); // Strips the trailing LF (0xA). - return l; - }; + // Note that on POSIX CRLF is not automatically translated into LF, so + // we need to strip CR (0xD) manually. + // + if (!l.empty () && l.back () == '\r') + l.pop_back (); - auto read_status = [&read_line, &status_code] () -> uint16_t - { - string l (read_line ()); + return l; + }; - for (;;) // Breakout loop. + auto read_status = [&read_line, &status_code] () -> uint16_t { - if (l.compare (0, 5, "HTTP/") != 0) - break; + string l (read_line ()); + + for (;;) // Breakout loop. + { + if (l.compare (0, 5, "HTTP/") != 0) + break; - size_t p (l.find (' ', 5)); // The protocol end. - if (p == string::npos) - break; + size_t p (l.find (' ', 5)); // The protocol end. + if (p == string::npos) + break; - p = l.find_first_not_of (' ', p + 1); // The code start. - if (p == string::npos) - break; + p = l.find_first_not_of (' ', p + 1); // The code start. + if (p == string::npos) + break; - size_t e (l.find (' ', p + 1)); // The code end. - if (e == string::npos) - break; + size_t e (l.find (' ', p + 1)); // The code end. + if (e == string::npos) + break; - uint16_t c (status_code (string (l, p, e - p))); - if (c == 0) - break; + uint16_t c (status_code (string (l, p, e - p))); + if (c == 0) + break; - return c; - } + return c; + } - throw_generic_error ( + throw_generic_error ( EINVAL, ("invalid HTTP response status line '" + l + "'").c_str ()); - }; - - uint16_t sc (read_status ()); + }; - if (sc == 100) - { - while (!read_line ().empty ()) ; // Skips the interim response. - sc = read_status (); // Reads the final status code. - } + uint16_t sc (read_status ()); - while (!read_line ().empty ()) ; // Skips headers. - - in.exceptions (es); + if (sc == 100) + { + while (!read_line ().empty ()) ; // Skips the interim response. + sc = read_status (); // Reads the final status code. + } - return sc; -} + while (!read_line ().empty ()) ; // Skips headers. -// Send a POST request to the GitHub API endpoint `ep`, parse GitHub's JSON -// response into `rs` (only for 200 codes), and return the HTTP status code. -// -// The endpoint `ep` should not have a leading slash. -// -// Pass additional HTTP headers in `hdrs`. For example: -// -// "HeaderName: header value" -// -template<typename T> -static uint16_t -github_post (T& rs, const string& ep, const brep::strings& hdrs) -{ - // Convert the header values to curl header option/value pairs. - // - brep::strings hdr_opts; + in.exceptions (es); - for (const string& h: hdrs) - { - hdr_opts.push_back ("--header"); - hdr_opts.push_back (h); + return sc; } - // Run curl. + // Send a POST request to the GitHub API endpoint `ep`, parse GitHub's JSON + // response into `rs` (only for 200 codes), and return the HTTP status code. + // + // The endpoint `ep` should not have a leading slash. + // + // Pass additional HTTP headers in `hdrs`. For example: // - try + // "HeaderName: header value" + // + template<typename T> + static uint16_t + github_post (T& rs, const string& ep, const strings& hdrs) { - // Pass --include to print the HTTP status line (followed by the response - // headers) so that we can get the response status code. - // - // Pass --no-fail to disable the --fail option added by butl::curl which - // causes curl to exit with status 22 in case of an error HTTP response - // status code (>= 400) otherwise we can't get the status code. - // - // Note that butl::curl also adds --location to make curl follow redirects - // (which is recommended by GitHub). - // - // The API version `2022-11-28` is the only one currently supported. If - // the X-GitHub-Api-Version header is not passed this version will be - // chosen by default. + // Convert the header values to curl header option/value pairs. // - // @@ TMP Although this request does not have a body, can't pass a nullfd - // stdin because it will cause butl::curl to fail if the method is - // POST. - // - fdpipe errp (fdopen_pipe ()); // stderr pipe. - - curl c (path ("-"), - path ("-"), // Write response to curl::in. - process::pipe (errp.in.get (), move (errp.out)), - curl::post, "https://api.github.com/" + ep, - "--include", // Output response headers for status code. - "--no-fail", // Don't exit with 22 if response status code >= 400. - "--header", "Accept: application/vnd.github+json", - "--header", "X-GitHub-Api-Version: 2022-11-28", - move (hdr_opts)); + strings hdr_opts; - ifdstream err (move (errp.in)); + for (const string& h: hdrs) + { + hdr_opts.push_back ("--header"); + hdr_opts.push_back (h); + } - // Parse the HTTP response. + // Run curl. // - int sc; // Status code. try { - ifdstream in (c.in.release (), fdstream_mode::skip); + // Pass --include to print the HTTP status line (followed by the response + // headers) so that we can get the response status code. + // + // Pass --no-fail to disable the --fail option added by butl::curl which + // causes curl to exit with status 22 in case of an error HTTP response + // status code (>= 400) otherwise we can't get the status code. + // + // Note that butl::curl also adds --location to make curl follow redirects + // (which is recommended by GitHub). + // + // The API version `2022-11-28` is the only one currently supported. If + // the X-GitHub-Api-Version header is not passed this version will be + // chosen by default. + // + // @@ TMP Although this request does not have a body, can't pass a nullfd + // stdin because it will cause butl::curl to fail if the method is + // POST. + // + fdpipe errp (fdopen_pipe ()); // stderr pipe. - c.out.close (); // No input required. + curl c (path ("-"), + path ("-"), // Write response to curl::in. + process::pipe (errp.in.get (), move (errp.out)), + curl::post, "https://api.github.com/" + ep, + "--include", // Output response headers for status code. + "--no-fail", // Don't exit with 22 if response status code >= 400. + "--header", "Accept: application/vnd.github+json", + "--header", "X-GitHub-Api-Version: 2022-11-28", + move (hdr_opts)); - // Read HTTP status code. - // - sc = read_status_code (in); + ifdstream err (move (errp.in)); - // Parse the response body if the status code is in the 200 range. + // Parse the HTTP response. // - if (sc >= 200 && sc < 300) + int sc; // Status code. + try { - json::parser p (in, ep); - rs = T (p); - } + ifdstream in (c.in.release (), fdstream_mode::skip); - in.close (); - } - catch (const brep::io_error& e) - { - // If the process exits with non-zero status, assume the IO error is due - // to that and fall through. - // - if (c.wait ()) + c.out.close (); // No input required. + + // Read HTTP status code. + // + sc = read_status_code (in); + + // Parse the response body if the status code is in the 200 range. + // + if (sc >= 200 && sc < 300) + { + json::parser p (in, ep); + rs = T (p); + } + + in.close (); + } + catch (const io_error& e) { - throw_generic_error ( - e.code ().value (), - (string ("unable to read curl stdout: ") + e.what ()).c_str ()); + // If the process exits with non-zero status, assume the IO error is due + // to that and fall through. + // + if (c.wait ()) + { + throw_generic_error ( + e.code ().value (), + (string ("unable to read curl stdout: ") + e.what ()).c_str ()); + } } - } - catch (const json::invalid_json_input& e) - { - // If the process exits with non-zero status, assume the JSON error is - // due to that and fall through. - // - if (c.wait ()) + catch (const json::invalid_json_input& e) { - throw_generic_error ( - EINVAL, - (string ("malformed JSON response from GitHub: ") + e.what ()) - .c_str ()); + // If the process exits with non-zero status, assume the JSON error is + // due to that and fall through. + // + if (c.wait ()) + { + throw_generic_error ( + EINVAL, + (string ("malformed JSON response from GitHub: ") + e.what ()) + .c_str ()); + } } - } - if (!c.wait ()) - { - string et (err.read_text ()); - throw_generic_error (EINVAL, - ("non-zero curl exit status: " + et).c_str ()); - } + if (!c.wait ()) + { + string et (err.read_text ()); + throw_generic_error (EINVAL, + ("non-zero curl exit status: " + et).c_str ()); + } - err.close (); + err.close (); - return sc; - } - catch (const process_error& e) - { - throw_generic_error ( + return sc; + } + catch (const process_error& e) + { + throw_generic_error ( e.code ().value (), (string ("unable to execute curl:") + e.what ()).c_str ()); + } + catch (const io_error& e) + { + // Unable to read diagnostics from stderr. + // + throw_generic_error ( + e.code ().value (), + (string ("unable to read curl stderr : ") + e.what ()).c_str ()); + } } - catch (const brep::io_error& e) - { - // Unable to read diagnostics from stderr. - // - throw_generic_error ( - e.code ().value (), - (string ("unable to read curl stderr : ") + e.what ()).c_str ()); - } -} - -bool brep::ci_github:: -handle (request& rq, response& rs) -{ - using namespace bpkg; - HANDLER_DIAG; + bool ci_github:: + handle (request& rq, response&) + { + using namespace bpkg; - // @@ TODO - if (false) - throw invalid_request (404, "CI request submission disabled"); + HANDLER_DIAG; - // Process headers. - // - string event; - { - bool content_type (false); + // @@ TODO + if (false) + throw invalid_request (404, "CI request submission disabled"); - for (const name_value& h: rq.headers ()) + // Process headers. + // + string event; { - if (icasecmp (h.name, "x-github-delivery") == 0) - { - // @@ TODO Check that delivery UUID has not been received before - // (replay attack). - } - else if (icasecmp (h.name, "content-type") == 0) - { - if (!h.value) - throw invalid_request (400, "missing content-type value"); + bool content_type (false); - if (icasecmp (*h.value, "application/json") != 0) + for (const name_value& h: rq.headers ()) + { + if (icasecmp (h.name, "x-github-delivery") == 0) { - throw invalid_request (400, - "invalid content-type value: '" + *h.value + - '\''); + // @@ TODO Check that delivery UUID has not been received before + // (replay attack). } + else if (icasecmp (h.name, "content-type") == 0) + { + if (!h.value) + throw invalid_request (400, "missing content-type value"); - content_type = true; - } - else if (icasecmp (h.name, "x-github-event") == 0) - { - if (!h.value) - throw invalid_request (400, "missing x-github-event value"); - - event = *h.value; - } - } + if (icasecmp (*h.value, "application/json") != 0) + { + throw invalid_request (400, + "invalid content-type value: '" + *h.value + + '\''); + } - if (!content_type) - throw invalid_request (400, "missing content-type header"); + content_type = true; + } + else if (icasecmp (h.name, "x-github-event") == 0) + { + if (!h.value) + throw invalid_request (400, "missing x-github-event value"); - if (event.empty ()) - throw invalid_request (400, "missing x-github-event header"); - } + event = *h.value; + } + } - // There is an event (specified in the x-github-event header) and each event - // contains a bunch of actions (specified in the JSON request body). - // - // Note: "GitHub continues to add new event types and new actions to - // existing event types." As a result we ignore known actions that we are - // not interested in and log and ignore unknown actions. The thinking here - // is that we want be "notified" of new actions at which point we can decide - // whether to ignore them or to handle. - // - if (event == "check_suite") - { - check_suite_event cs; - try - { - json::parser p (rq.content (64 * 1024), "check_suite webhook"); + if (!content_type) + throw invalid_request (400, "missing content-type header"); - cs = check_suite_event (p); - } - catch (const json::invalid_json_input& e) - { - throw invalid_request (400, "malformed JSON in request body"); + if (event.empty ()) + throw invalid_request (400, "missing x-github-event header"); } - // @@ TODO: log and ignore unknown. + // There is an event (specified in the x-github-event header) and each event + // contains a bunch of actions (specified in the JSON request body). // - if (cs.action == "requested") - { - } - else if (cs.action == "rerequested") - { - // Someone manually requested to re-run the check runs in this check - // suite. - } - else if (cs.action == "completed") + // Note: "GitHub continues to add new event types and new actions to + // existing event types." As a result we ignore known actions that we are + // not interested in and log and ignore unknown actions. The thinking here + // is that we want be "notified" of new actions at which point we can decide + // whether to ignore them or to handle. + // + if (event == "check_suite") { - // GitHub thinks that "all the check runs in this check suite have - // completed and a conclusion is available". Looks like this one we - // ignore? - } - else - throw invalid_request (400, "unsupported action: " + cs.action); + check_suite_event cs; + try + { + json::parser p (rq.content (64 * 1024), "check_suite webhook"); - cout << "<check_suite webhook>" << endl << cs << endl; + cs = check_suite_event (p); + } + catch (const json::invalid_json_input& e) + { + throw invalid_request (400, "malformed JSON in request body"); + } - string jwt; - try - { - // Set token's "issued at" time 60 seconds in the past to combat clock - // drift (as recommended by GitHub). + // @@ TODO: log and ignore unknown. // - jwt = gen_jwt ( + if (cs.action == "requested") + { + } + else if (cs.action == "rerequested") + { + // Someone manually requested to re-run the check runs in this check + // suite. + } + else if (cs.action == "completed") + { + // GitHub thinks that "all the check runs in this check suite have + // completed and a conclusion is available". Looks like this one we + // ignore? + } + else + throw invalid_request (400, "unsupported action: " + cs.action); + + cout << "<check_suite webhook>" << endl << cs << endl; + + string jwt; + try + { + // Set token's "issued at" time 60 seconds in the past to combat clock + // drift (as recommended by GitHub). + // + jwt = gen_jwt ( *options_, options_->ci_github_app_private_key (), to_string (options_->ci_github_app_id ()), chrono::seconds (options_->ci_github_jwt_validity_period ()), chrono::seconds (60)); - cout << "JWT: " << jwt << endl; - } - catch (const system_error& e) - { - fail << "unable to generate JWT (errno=" << e.code () << "): " - << e.what (); - } - - // Authenticate to GitHub as an app installation. - // - installation_access_token iat; - try - { - // API endpoint. - // - string ep ("app/installations/" + to_string (cs.installation.id) + - "/access_tokens"); - - int sc (github_post (iat, ep, strings {"Authorization: Bearer " + jwt})); + cout << "JWT: " << jwt << endl; + } + catch (const system_error& e) + { + fail << "unable to generate JWT (errno=" << e.code () << "): " + << e.what (); + } - // Possible response status codes from the access_tokens endpoint: - // - // 201 Created - // 401 Requires authentication - // 403 Forbidden - // 404 Resource not found - // 422 Validation failed, or the endpoint has been spammed. + // Authenticate to GitHub as an app installation. // - // Note that the payloads of non-201 status codes are undocumented. - // - if (sc != 201) + installation_access_token iat; + try + { + // API endpoint. + // + string ep ("app/installations/" + to_string (cs.installation.id) + + "/access_tokens"); + + int sc (github_post (iat, ep, strings {"Authorization: Bearer " + jwt})); + + // Possible response status codes from the access_tokens endpoint: + // + // 201 Created + // 401 Requires authentication + // 403 Forbidden + // 404 Resource not found + // 422 Validation failed, or the endpoint has been spammed. + // + // Note that the payloads of non-201 status codes are undocumented. + // + if (sc != 201) + { + throw runtime_error ("error status code received from GitHub: " + + to_string (sc)); + } + } + catch (const system_error& e) { - throw runtime_error ("error status code received from GitHub: " + - to_string (sc)); + fail << "unable to get installation access token (errno=" << e.code () + << "): " << e.what (); } + + cout << endl << "<installation_access_token>" << endl << iat << endl; + + return true; } - catch (const system_error& e) + else if (event == "pull_request") { - fail << "unable to get installation access token (errno=" << e.code () - << "): " << e.what (); + throw invalid_request (501, "pull request events not implemented yet"); } - - cout << endl << "<installation_access_token>" << endl << iat << endl; - - return true; - } - else if (event == "pull_request") - { - throw invalid_request (501, "pull request events not implemented yet"); + else + throw invalid_request (400, "unexpected event: '" + event + "'"); } - else - throw invalid_request (400, "unexpected event: '" + event + "'"); -} -using event = json::event; + using event = json::event; -// check_suite -// -check_suite:: -check_suite (json::parser& p) -{ - p.next_expect (event::begin_object); - - // Skip unknown/uninteresting members. + // check_suite // - while (p.next_expect (event::name, event::end_object)) + gh::check_suite:: + check_suite (json::parser& p) { - const string& n (p.name ()); - - if (n == "id") id = p.next_expect_number<uint64_t> (); - else if (n == "head_branch") head_branch = p.next_expect_string (); - else if (n == "head_sha") head_sha = p.next_expect_string (); - else if (n == "before") before = p.next_expect_string (); - else if (n == "after") after = p.next_expect_string (); - else p.next_expect_value_skip (); - } -} + p.next_expect (event::begin_object); -static ostream& -operator<< (ostream& os, const check_suite& cs) -{ - os << "id: " << cs.id << endl - << "head_branch: " << cs.head_branch << endl - << "head_sha: " << cs.head_sha << endl - << "before: " << cs.before << endl - << "after: " << cs.after << endl; + // Skip unknown/uninteresting members. + // + while (p.next_expect (event::name, event::end_object)) + { + const string& n (p.name ()); + + if (n == "id") id = p.next_expect_number<uint64_t> (); + else if (n == "head_branch") head_branch = p.next_expect_string (); + else if (n == "head_sha") head_sha = p.next_expect_string (); + else if (n == "before") before = p.next_expect_string (); + else if (n == "after") after = p.next_expect_string (); + else p.next_expect_value_skip (); + } + } - return os; -} + static ostream& + gh::operator<< (ostream& os, const check_suite& cs) + { + os << "id: " << cs.id << endl + << "head_branch: " << cs.head_branch << endl + << "head_sha: " << cs.head_sha << endl + << "before: " << cs.before << endl + << "after: " << cs.after << endl; -// repository -// -repository:: -repository (json::parser& p) -{ - p.next_expect (event::begin_object); + return os; + } - // Skip unknown/uninteresting members. + // repository // - while (p.next_expect (event::name, event::end_object)) + gh::repository:: + repository (json::parser& p) { - const string& n (p.name ()); - - if (n == "name") name = p.next_expect_string (); - else if (n == "full_name") full_name = p.next_expect_string (); - else if (n == "default_branch") default_branch = p.next_expect_string (); - else p.next_expect_value_skip (); - } -} + p.next_expect (event::begin_object); -static ostream& -operator<< (ostream& os, const repository& rep) -{ - os << "name: " << rep.name << endl - << "full_name: " << rep.full_name << endl - << "default_branch: " << rep.default_branch << endl; + // Skip unknown/uninteresting members. + // + while (p.next_expect (event::name, event::end_object)) + { + const string& n (p.name ()); - return os; -} + if (n == "name") name = p.next_expect_string (); + else if (n == "full_name") full_name = p.next_expect_string (); + else if (n == "default_branch") default_branch = p.next_expect_string (); + else p.next_expect_value_skip (); + } + } -// installation + static ostream& + gh::operator<< (ostream& os, const repository& rep) + { + os << "name: " << rep.name << endl + << "full_name: " << rep.full_name << endl + << "default_branch: " << rep.default_branch << endl; -installation:: -installation (json::parser& p) -{ - p.next_expect (event::begin_object); + return os; + } - // Skip unknown/uninteresting members. + // installation // - while (p.next_expect (event::name, event::end_object)) + gh::installation:: + installation (json::parser& p) { - const string& n (p.name ()); + p.next_expect (event::begin_object); - if (n == "id") id = p.next_expect_number<uint64_t> (); - else p.next_expect_value_skip (); - } -} + // Skip unknown/uninteresting members. + // + while (p.next_expect (event::name, event::end_object)) + { + const string& n (p.name ()); -static ostream& -operator<< (ostream& os, const installation& i) -{ - os << "id: " << i.id << endl; + if (n == "id") id = p.next_expect_number<uint64_t> (); + else p.next_expect_value_skip (); + } + } - return os; -} + static ostream& + gh::operator<< (ostream& os, const installation& i) + { + os << "id: " << i.id << endl; -// check_suite_event -// -check_suite_event:: -check_suite_event (json::parser& p) -{ - p.next_expect (event::begin_object); + return os; + } - // Skip unknown/uninteresting members. + // check_suite_event // - while (p.next_expect (event::name, event::end_object)) + gh::check_suite_event:: + check_suite_event (json::parser& p) { - const string& n (p.name ()); + p.next_expect (event::begin_object); - if (n == "action") action = p.next_expect_string (); - else if (n == "check_suite") check_suite = ::check_suite (p); - else if (n == "repository") repository = ::repository (p); - else if (n == "installation") installation = ::installation (p); - else p.next_expect_value_skip (); - } -} + // Skip unknown/uninteresting members. + // + while (p.next_expect (event::name, event::end_object)) + { + const string& n (p.name ()); -static ostream& -operator<< (ostream& os, const check_suite_event& cs) -{ - os << "action: " << cs.action << endl; - os << "<check_suite>" << endl << cs.check_suite; - os << "<repository>" << endl << cs.repository; - os << "<installation>" << endl << cs.installation; + if (n == "action") action = p.next_expect_string (); + else if (n == "check_suite") check_suite = gh::check_suite (p); + else if (n == "repository") repository = gh::repository (p); + else if (n == "installation") installation = gh::installation (p); + else p.next_expect_value_skip (); + } + } - return os; -} + static ostream& + gh::operator<< (ostream& os, const check_suite_event& cs) + { + os << "action: " << cs.action << endl; + os << "<check_suite>" << endl << cs.check_suite; + os << "<repository>" << endl << cs.repository; + os << "<installation>" << endl << cs.installation; -// installation_access_token -// -// Example JSON: -// -// { -// "token": "ghs_Py7TPcsmsITeVCAWeVtD8RQs8eSos71O5Nzp", -// "expires_at": "2024-02-15T16:16:38Z", -// ... -// } -// -installation_access_token:: -installation_access_token (json::parser& p) -{ - p.next_expect (event::begin_object); + return os; + } - // Skip unknown/uninteresting members. + // installation_access_token + // + // Example JSON: // - while (p.next_expect (event::name, event::end_object)) + // { + // "token": "ghs_Py7TPcsmsITeVCAWeVtD8RQs8eSos71O5Nzp", + // "expires_at": "2024-02-15T16:16:38Z", + // ... + // } + // + gh::installation_access_token:: + installation_access_token (json::parser& p) { - const string& n (p.name ()); + p.next_expect (event::begin_object); - if (n == "token") - token = p.next_expect_string (); - else if (n == "expires_at") + // Skip unknown/uninteresting members. + // + while (p.next_expect (event::name, event::end_object)) { - const string& s (p.next_expect_string ()); - expires_at = from_string (s.c_str (), "%Y-%m-%dT%TZ", false /* local */); + const string& n (p.name ()); + + if (n == "token") + token = p.next_expect_string (); + else if (n == "expires_at") + { + const string& s (p.next_expect_string ()); + expires_at = from_string (s.c_str (), "%Y-%m-%dT%TZ", false /* local */); + } + else p.next_expect_value_skip (); } - else p.next_expect_value_skip (); } -} -static ostream& -operator<< (ostream& os, const installation_access_token& t) -{ - os << "token: " << t.token << endl; - os << "expires_at: " << t.expires_at << endl; + static ostream& + gh::operator<< (ostream& os, const installation_access_token& t) + { + os << "token: " << t.token << endl; + os << "expires_at: "; + butl::operator<< (os, t.expires_at) << endl; - return os; + return os; + } } |