diff options
Diffstat (limited to 'src/regex101.client.cc')
-rw-r--r-- | src/regex101.client.cc | 331 |
1 files changed, 331 insertions, 0 deletions
diff --git a/src/regex101.client.cc b/src/regex101.client.cc new file mode 100644 index 0000000..73871cc --- /dev/null +++ b/src/regex101.client.cc @@ -0,0 +1,331 @@ +/** + * Copyright (c) 2022, Timothy Stack + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * * Neither the name of Timothy Stack nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "regex101.client.hh" + +#include <curl/curl.h> + +#include "config.h" +#include "curl_looper.hh" +#include "ghc/filesystem.hpp" +#include "yajlpp/yajlpp_def.hh" + +namespace regex101 { +namespace client { + +static const json_path_handler_base::enum_value_t CRITERIA_ENUM[] = { + {"DOES_MATCH", unit_test::criteria::DOES_MATCH}, + {"DOES_NOT_MATCH", unit_test::criteria::DOES_NOT_MATCH}, + + json_path_handler_base::ENUM_TERMINATOR, +}; + +static const json_path_container UNIT_TEST_HANDLERS = { + yajlpp::property_handler("description") + .for_field(&unit_test::ut_description), + yajlpp::property_handler("testString") + .for_field(&unit_test::ut_test_string), + yajlpp::property_handler("target").for_field(&unit_test::ut_target), + yajlpp::property_handler("criteria") + .with_enum_values(CRITERIA_ENUM) + .for_field(&unit_test::ut_criteria), +}; + +static const typed_json_path_container<entry> ENTRY_HANDLERS = { + yajlpp::property_handler("regex").for_field(&entry::e_regex), + yajlpp::property_handler("testString").for_field(&entry::e_test_string), + yajlpp::property_handler("flags").for_field(&entry::e_flags), + yajlpp::property_handler("delimiter").for_field(&entry::e_delimiter), + yajlpp::property_handler("flavor").for_field(&entry::e_flavor), + yajlpp::property_handler("unitTests#") + .for_field(&entry::e_unit_tests) + .with_children(UNIT_TEST_HANDLERS), + yajlpp::property_handler("permalinkFragment") + .for_field(&entry::e_permalink_fragment), +}; + +static const typed_json_path_container<upsert_response> RESPONSE_HANDLERS = { + yajlpp::property_handler("deleteCode") + .for_field(&upsert_response::cr_delete_code), + yajlpp::property_handler("permalinkFragment") + .for_field(&upsert_response::cr_permalink_fragment), + yajlpp::property_handler("version").for_field(&upsert_response::cr_version), +}; + +static const ghc::filesystem::path REGEX101_BASE_URL + = "https://regex101.com/api/regex"; +static const char* USER_AGENT = "lnav/" PACKAGE_VERSION; + +Result<upsert_response, lnav::console::user_message> +upsert(entry& en) +{ + auto entry_json = ENTRY_HANDLERS.to_string(en); + + curl_request cr(REGEX101_BASE_URL.string()); + + curl_easy_setopt(cr, CURLOPT_URL, cr.get_name().c_str()); + curl_easy_setopt(cr, CURLOPT_POST, 1); + curl_easy_setopt(cr, CURLOPT_POSTFIELDS, entry_json.c_str()); + curl_easy_setopt(cr, CURLOPT_POSTFIELDSIZE, entry_json.size()); + curl_easy_setopt(cr, CURLOPT_USERAGENT, USER_AGENT); + + auto_mem<curl_slist> list(curl_slist_free_all); + + list = curl_slist_append(list, "Content-Type: application/json"); + + curl_easy_setopt(cr, CURLOPT_HTTPHEADER, list.in()); + + auto perform_res = cr.perform(); + if (perform_res.isErr()) { + return Err( + lnav::console::user_message::error( + "unable to create entry on regex101.com") + .with_reason(curl_easy_strerror(perform_res.unwrapErr()))); + } + + auto response = perform_res.unwrap(); + auto resp_code = cr.get_response_code(); + if (resp_code != 200) { + return Err(lnav::console::user_message::error( + "unable to create entry on regex101.com") + .with_reason(attr_line_t() + .append("received response code ") + .append(lnav::roles::number( + fmt::to_string(resp_code))) + .append(" content ") + .append_quoted(response))); + } + + auto parse_res + = RESPONSE_HANDLERS.parser_for(intern_string::lookup(cr.get_name())) + .of(response); + if (parse_res.isOk()) { + return Ok(parse_res.unwrap()); + } + + auto errors = parse_res.unwrapErr(); + return Err(lnav::console::user_message::error( + "unable to create entry on regex101.com") + .with_reason(errors[0].to_attr_line({}))); +} + +struct retrieve_entity { + std::string re_permalink_fragment; + int32_t re_versions{1}; +}; + +static const typed_json_path_container<retrieve_entity> RETRIEVE_ENTITY_HANDLERS + = { + yajlpp::property_handler("permalinkFragment") + .for_field(&retrieve_entity::re_permalink_fragment), + yajlpp::property_handler("versions") + .for_field(&retrieve_entity::re_versions), +}; + +retrieve_result_t +retrieve(const std::string& permalink) +{ + auto entry_url = REGEX101_BASE_URL / permalink; + curl_request entry_req(entry_url.string()); + + curl_easy_setopt(entry_req, CURLOPT_URL, entry_req.get_name().c_str()); + curl_easy_setopt(entry_req, CURLOPT_USERAGENT, USER_AGENT); + + auto perform_res = entry_req.perform(); + if (perform_res.isErr()) { + return lnav::console::user_message::error( + attr_line_t("unable to get entry ") + .append_quoted(lnav::roles::symbol(permalink)) + .append(" on regex101.com")) + .with_reason(curl_easy_strerror(perform_res.unwrapErr())); + } + + auto response = perform_res.unwrap(); + auto resp_code = entry_req.get_response_code(); + if (resp_code == 404) { + return no_entry{}; + } + if (resp_code != 200) { + return lnav::console::user_message::error( + attr_line_t("unable to get entry ") + .append_quoted(lnav::roles::symbol(permalink)) + .append(" on regex101.com")) + .with_reason( + attr_line_t() + .append("received response code ") + .append(lnav::roles::number(fmt::to_string(resp_code))) + .append(" content ") + .append_quoted(response)); + } + + auto parse_res + = RETRIEVE_ENTITY_HANDLERS + .parser_for(intern_string::lookup(entry_req.get_name())) + .of(response); + + if (parse_res.isErr()) { + auto parse_errors = parse_res.unwrapErr(); + + return lnav::console::user_message::error( + attr_line_t("unable to get entry ") + .append_quoted(lnav::roles::symbol(permalink)) + .append(" on regex101.com")) + .with_reason(parse_errors[0].to_attr_line({})); + } + + auto entry_value = parse_res.unwrap(); + + if (entry_value.re_versions == 0) { + return no_entry{}; + } + + auto version_url = entry_url / fmt::to_string(entry_value.re_versions); + curl_request version_req(version_url.string()); + + curl_easy_setopt(version_req, CURLOPT_URL, version_req.get_name().c_str()); + curl_easy_setopt(version_req, CURLOPT_USERAGENT, USER_AGENT); + + auto version_perform_res = version_req.perform(); + if (version_perform_res.isErr()) { + return lnav::console::user_message::error( + attr_line_t("unable to get entry version ") + .append_quoted(lnav::roles::symbol(version_url.string())) + .append(" on regex101.com")) + .with_reason(curl_easy_strerror(version_perform_res.unwrapErr())); + } + + auto version_response = version_perform_res.unwrap(); + auto version_parse_res + = ENTRY_HANDLERS + .parser_for(intern_string::lookup(version_req.get_name())) + .of(version_response); + + if (version_parse_res.isErr()) { + auto parse_errors = version_parse_res.unwrapErr(); + return lnav::console::user_message::error( + attr_line_t("unable to get entry version ") + .append_quoted(lnav::roles::symbol(version_url.string())) + .append(" on regex101.com")) + .with_reason(parse_errors[0].to_attr_line({})); + } + + auto retval = version_parse_res.unwrap(); + + retval.e_permalink_fragment = permalink; + + return retval; +} + +struct delete_entity { + std::string de_delete_code; +}; + +static const typed_json_path_container<delete_entity> DELETE_ENTITY_HANDLERS = { + yajlpp::property_handler("deleteCode") + .for_field(&delete_entity::de_delete_code), +}; + +Result<void, lnav::console::user_message> +delete_entry(const std::string& delete_code) +{ + curl_request cr(REGEX101_BASE_URL.string()); + delete_entity entity{delete_code}; + auto entity_json = DELETE_ENTITY_HANDLERS.to_string(entity); + + curl_easy_setopt(cr, CURLOPT_URL, cr.get_name().c_str()); + curl_easy_setopt(cr, CURLOPT_CUSTOMREQUEST, "DELETE"); + curl_easy_setopt(cr, CURLOPT_USERAGENT, USER_AGENT); + curl_easy_setopt(cr, CURLOPT_POSTFIELDS, entity_json.c_str()); + curl_easy_setopt(cr, CURLOPT_POSTFIELDSIZE, entity_json.size()); + + auto_mem<curl_slist> list(curl_slist_free_all); + + list = curl_slist_append(list, "Content-Type: application/json"); + + curl_easy_setopt(cr, CURLOPT_HTTPHEADER, list.in()); + + auto perform_res = cr.perform(); + if (perform_res.isErr()) { + return Err( + lnav::console::user_message::error( + "unable to delete entry on regex101.com") + .with_reason(curl_easy_strerror(perform_res.unwrapErr()))); + } + + auto response = perform_res.unwrap(); + auto resp_code = cr.get_response_code(); + if (resp_code != 200) { + return Err(lnav::console::user_message::error( + "unable to delete entry on regex101.com") + .with_reason(attr_line_t() + .append("received response code ") + .append(lnav::roles::number( + fmt::to_string(resp_code))) + .append(" content ") + .append_quoted(response))); + } + + return Ok(); +} + +std::string +to_edit_url(const std::string& permalink) +{ + return fmt::format(FMT_STRING("https://regex101.com/r/{}"), permalink); +} + +bool +unit_test::operator==(const unit_test& rhs) const +{ + return ut_description == rhs.ut_description + && ut_test_string == rhs.ut_test_string && ut_target == rhs.ut_target + && ut_criteria == rhs.ut_criteria; +} + +bool +unit_test::operator!=(const unit_test& rhs) const +{ + return !(rhs == *this); +} + +bool +entry::operator==(const entry& rhs) const +{ + return e_regex == rhs.e_regex && e_test_string == rhs.e_test_string + && e_flavor == rhs.e_flavor && e_unit_tests == rhs.e_unit_tests; +} + +bool +entry::operator!=(const entry& rhs) const +{ + return !(rhs == *this); +} + +} // namespace client +} // namespace regex101 |