diff --git a/exe/generate.cc b/exe/generate.cc index ccd56a9..8902e6b 100644 --- a/exe/generate.cc +++ b/exe/generate.cc @@ -16,4 +16,4 @@ int main(int argc, char** argv) { auto source = std::ofstream{argv[3]}; openapi::write_types(root, argv[2], header, source, std::string_view{argv[4]}); -} \ No newline at end of file +} diff --git a/include/openapi/gen_types.h b/include/openapi/gen_types.h index 6705ba5..4114c34 100644 --- a/include/openapi/gen_types.h +++ b/include/openapi/gen_types.h @@ -50,7 +50,14 @@ void gen_member_init(YAML::Node const& root, std::ostream& header, std::ostream& source); +void gen_member_init_from_seg(YAML::Node const& root, + YAML::Node const& x, + std::size_t seg_idx, + std::ostream& header, + std::ostream& source); + void write_params(YAML::Node const& root, + YAML::Node const& path, YAML::Node const&, std::ostream& header, std::ostream& source); @@ -61,4 +68,4 @@ void write_types(YAML::Node const&, std::ostream& source, std::optional ns); -} // namespace openapi \ No newline at end of file +} // namespace openapi diff --git a/include/openapi/parse.h b/include/openapi/parse.h index 9f8fe23..2ffaf42 100644 --- a/include/openapi/parse.h +++ b/include/openapi/parse.h @@ -1,6 +1,7 @@ #pragma once #include "boost/url/params_view.hpp" +#include "boost/url/segments_view.hpp" #include #include @@ -67,4 +68,20 @@ T parse_param(boost::urls::params_view const& params, return default_value.has_value() ? T{*default_value} : T{}; } -} // namespace openapi \ No newline at end of file +template +T parse_segment(boost::urls::segments_view const& segs, + std::string_view name, + std::size_t idx) { + auto it = segs.begin(); + if (idx < segs.size()) { + std::advance(it, idx); + auto v = T{}; + parse(*it, v); + return v; + } else { + throw bad_request_exception{ + fmt::format("missing segment parameter: {}", name)}; + } +} + +} // namespace openapi diff --git a/src/gen_types.cc b/src/gen_types.cc index e390004..a6d9bed 100644 --- a/src/gen_types.cc +++ b/src/gen_types.cc @@ -2,6 +2,7 @@ #include #include +#include #include "utl/enumerate.h" @@ -108,7 +109,7 @@ std::string_view to_cpp(type const t) { struct indent { explicit indent(int indent, char separator = ',') - : indent_{indent}, separator_{separator} {} + : separator_{separator}, indent_{indent} {} void operator()(std::ostream& out) { if (!first_ && separator_ != '\0') { @@ -314,10 +315,37 @@ void gen_member_init(YAML::Node const& root, out << ", allow_missing)}"; } +void gen_member_init_from_seg(YAML::Node const& root, + YAML::Node const& x, + std::size_t idx, + std::ostream& out) { + auto const schema = x["schema"]; + auto const name = x["name"].as(); + auto const type = get_type(root, name, schema, true); + out << " " << name << "_{::openapi::parse_segment<" << type << ">(segs, \"" + << name << "\", " << idx << ")}"; +} + void write_params(YAML::Node const& root, + YAML::Node const& path, YAML::Node const& n, std::ostream& header, std::ostream& source) { + const auto p = path.as(); + auto segs = p | std::views::split('/'); + auto seg_idx = std::unordered_map{}; + + for (auto const& [i, seg] : segs | std::views::enumerate) { + auto sv = std::string_view{seg.begin(), seg.end()}; + if (sv.starts_with('{')) { + sv.remove_prefix(1); + } + if (sv.ends_with('}')) { + sv.remove_suffix(1); + } + seg_idx.emplace(sv, i - 1); + } + for (auto const& p : n["parameters"]) { auto const name = p["name"].as(); auto const items = p["schema"]["items"]; @@ -333,12 +361,14 @@ void write_params(YAML::Node const& root, header << "struct " << id << " {\n"; header << " explicit " << id << "();\n"; header << " explicit " << id - << "(boost::urls::params_view const&, bool allow_missing = false);\n"; + << "(boost::urls::params_view const&, boost::urls::segments_view " + "const& = {}, bool allow_missing = false);\n"; header << " boost::urls::url to_url(std::string_view path) const;\n"; source << id << "::" << id << "() = default;\n"; source << id << "::" << id - << "(boost::urls::params_view const& params, bool allow_missing)"; + << "(boost::urls::params_view const& params, " + "boost::urls::segments_view const& segs, bool allow_missing)"; auto const parameters = n["parameters"]; if (parameters.IsDefined() && parameters.size() != 0) { @@ -346,7 +376,12 @@ void write_params(YAML::Node const& root, auto ind = indent{2}; for (auto const& p : parameters) { ind(source); - gen_member_init(root, p, is_required(p), source); + if (p["in"].IsDefined() && p["in"].as() == "path") { + auto const idx = seg_idx.at(p["name"].as()); + gen_member_init_from_seg(root, p, idx, source); + } else { + gen_member_init(root, p, is_required(p), source); + } } } source << "\n {}\n\n"; @@ -357,6 +392,9 @@ void write_params(YAML::Node const& root, source << " auto u = boost::urls::url{path};\n"; source << " auto default_val = " << id << "{};\n"; for (auto const& p : parameters) { + if (p["in"].IsDefined() && p["in"].as() == "path") { + continue; + } auto const name = p["name"].as(); auto const schema = p["schema"]; auto const has_default = schema["default"].IsDefined(); @@ -402,7 +440,9 @@ void write_params(YAML::Node const& root, for (auto const& p : n["parameters"]) { auto const name = p["name"].as(); - gen_member(root, name, is_required(p), p["schema"], header); + auto const in_path = + p["in"].IsDefined() && p["in"].as() == "path"; + gen_member(root, name, in_path || is_required(p), p["schema"], header); } header << "};\n\n"; } @@ -560,7 +600,7 @@ void write_types(YAML::Node const& root, for (auto const& path : root["paths"]) { for (auto const& method : path.second) { - write_params(root, method.second, header, source); + write_params(root, path.first, method.second, header, source); for (auto const& response : method.second["responses"]) { gen_type(method.second["operationId"].as() + "_response", @@ -573,4 +613,4 @@ void write_types(YAML::Node const& root, write_postlude(header, source, ns); } -} // namespace openapi \ No newline at end of file +} // namespace openapi diff --git a/test/generate_types_test.cc b/test/generate_types_test.cc index d24dcfc..1deee99 100644 --- a/test/generate_types_test.cc +++ b/test/generate_types_test.cc @@ -65,4 +65,22 @@ TEST(openapi, parse_expect_default_value) { boost::urls::url_view{"/"}.params(), "mode", std::vector{mode::TRANSIT, mode::WALK}); EXPECT_EQ((std::vector{mode::TRANSIT, mode::WALK}), v); -} \ No newline at end of file +} + +TEST(openapi, parse_segment_string) { + auto url = boost::urls::url_view{"/items/hello/42"}; + auto v = std::string{}; + parse_segment(url.segments(), "param1", 1); + EXPECT_EQ("hello", parse_segment(url.segments(), "param1", 1)); +} + +TEST(openapi, parse_segment_int) { + auto url = boost::urls::url_view{"/items/hello/42"}; + EXPECT_EQ(42, parse_segment(url.segments(), "param2", 2)); +} + +TEST(openapi, parse_segment_out_of_bounds) { + auto url = boost::urls::url_view{"/items/hello"}; + EXPECT_THROW(parse_segment(url.segments(), "param1", 5), + bad_request_exception); +} diff --git a/test/parameter_test.cc b/test/parameter_test.cc new file mode 100644 index 0000000..a360e2c --- /dev/null +++ b/test/parameter_test.cc @@ -0,0 +1,33 @@ +#include "gtest/gtest.h" + +#include "openapi/bad_request_exception.h" +#include "yaml-cpp/yaml.h" + +#include "cista/hash.h" + +#include "boost/json.hpp" +#include "boost/url.hpp" + +#include "utl/verify.h" + +#include "openapi/gen_types.h" +#include "openapi/json.h" + +#include "pet-api/pet-api.h" + +using namespace openapi; +using namespace pet; + +TEST(openapi, valid_query) { + auto url = boost::urls::url_view{"/items/test/42?param3=10"}; + auto p = pet::getItems_params{url.params(), url.segments()}; + EXPECT_EQ("test", p.param1_); + EXPECT_EQ(42, p.param2_); + EXPECT_EQ(10, p.param3_); +} + +TEST(openapi, path_params_always_required) { + auto url = boost::urls::url_view{"/items?param3=10"}; + EXPECT_THROW(pet::getItems_params(url.params(), url.segments()), + openapi::bad_request_exception); +} diff --git a/test/pet.yml b/test/pet.yml index 16507f8..8c425d9 100644 --- a/test/pet.yml +++ b/test/pet.yml @@ -1,7 +1,22 @@ paths: - /items: + /items/{param1}/{param2}: get: operationId: getItems + parameters: + - name: param1 + required: true + in: path + schema: + type: string + - name: param2 + required: true + in: path + schema: + type: integer + - name: param3 + schema: + type: integer + responses: 200: content: @@ -10,7 +25,6 @@ paths: type: array items: $ref: '#/components/schemas/Item' - components: schemas: Status: @@ -38,4 +52,4 @@ components: y: $ref: '#/components/schemas/Pets' z: - type: integer \ No newline at end of file + type: integer