From 24394114184e836bb663b73122ca51ee501e4bdf Mon Sep 17 00:00:00 2001 From: Blacksmoke16 Date: Sat, 29 Dec 2018 23:39:42 -0500 Subject: [PATCH] initial prototype --- .editorconfig | 9 ++ .gitignore | 9 ++ .travis.yml | 1 + LICENSE | 21 +++ README.md | 8 +- shard.yml | 19 +++ spec/controllers/athena_controller.cr | 33 +++++ spec/controllers/bool_controller.cr | 19 +++ spec/controllers/float_controller.cr | 37 ++++++ spec/controllers/int_controller.cr | 181 ++++++++++++++++++++++++++ spec/controllers/string_controller.cr | 19 +++ spec/controllers/struct_controller.cr | 19 +++ spec/controllers/user_controller.cr | 13 ++ spec/get_spec.cr | 89 +++++++++++++ spec/post_spec.cr | 89 +++++++++++++ spec/spec_helper.cr | 8 ++ src/athena.cr | 57 ++++++++ src/converters.cr | 9 ++ src/route_handler.cr | 92 +++++++++++++ src/types.cr | 51 ++++++++ 20 files changed, 779 insertions(+), 4 deletions(-) create mode 100644 .editorconfig create mode 100644 .gitignore create mode 100644 .travis.yml create mode 100644 LICENSE create mode 100644 shard.yml create mode 100644 spec/controllers/athena_controller.cr create mode 100644 spec/controllers/bool_controller.cr create mode 100644 spec/controllers/float_controller.cr create mode 100644 spec/controllers/int_controller.cr create mode 100644 spec/controllers/string_controller.cr create mode 100644 spec/controllers/struct_controller.cr create mode 100644 spec/controllers/user_controller.cr create mode 100644 spec/get_spec.cr create mode 100644 spec/post_spec.cr create mode 100644 spec/spec_helper.cr create mode 100644 src/athena.cr create mode 100644 src/converters.cr create mode 100644 src/route_handler.cr create mode 100644 src/types.cr diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..163eb75c8 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*.cr] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +indent_style = space +indent_size = 2 +trim_trailing_whitespace = true diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..e29dae78f --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +/docs/ +/lib/ +/bin/ +/.shards/ +*.dwarf + +# Libraries don't need dependency lock +# Dependencies will be locked in application that uses them +/shard.lock diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 000000000..ffc7b6ac5 --- /dev/null +++ b/.travis.yml @@ -0,0 +1 @@ +language: crystal diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000..aee7ee609 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2018 your-name-here + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md index c37e6ec59..e9d72cb02 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # athena -TODO: Write a description here +Annotation based web framework with built in param conversion. ## Installation @@ -8,7 +8,7 @@ TODO: Write a description here ```yaml dependencies: athena: - github: your-github-user/athena + github: Blacksmoke16/athena ``` 2. Run `shards install` @@ -26,7 +26,7 @@ TODO: Write development instructions here ## Contributing -1. Fork it () +1. Fork it () 2. Create your feature branch (`git checkout -b my-new-feature`) 3. Commit your changes (`git commit -am 'Add some feature'`) 4. Push to the branch (`git push origin my-new-feature`) @@ -34,4 +34,4 @@ TODO: Write development instructions here ## Contributors -- [your-name-here](https://github.com/your-github-user) - creator and maintainer +- [Blacksmoke16](https://github.com/Blacksmoke16) Blacksmoke16 - creator, maintainer \ No newline at end of file diff --git a/shard.yml b/shard.yml new file mode 100644 index 000000000..88cbc832a --- /dev/null +++ b/shard.yml @@ -0,0 +1,19 @@ +name: athena + +description: | + Annotation based web framework with built in param conversion. + +version: 0.1.0 + +authors: + - Blacksmoke16 + +crystal: 0.27.0 + +license: MIT + +dependencies: + CrSerializer: + github: blacksmoke16/CrSerializer + radix: + github: luislavena/radix \ No newline at end of file diff --git a/spec/controllers/athena_controller.cr b/spec/controllers/athena_controller.cr new file mode 100644 index 000000000..ac5e69940 --- /dev/null +++ b/spec/controllers/athena_controller.cr @@ -0,0 +1,33 @@ +class AthenaController < Athena::ClassController + @[Athena::Get(path: "noParamsGet")] + def self.noParamsGet : String + "foobar" + end + + @[Athena::Get(path: "double/:val1/:val2")] + def self.doubleParams(val1 : Int32, val2 : Int32) : Int32 + it "should run correctly" do + val1.should be_a Int32 + val2.should be_a Int32 + val1.should eq 1000 + val2.should eq 9000 + end + val1 + val2 + end + + @[Athena::Post(path: "noParamsPost")] + def self.noParamsPost : String + "foobar" + end + + @[Athena::Post(path: "double/:val")] + def self.doubleParamsPost(val1 : Int32, val2 : Int32) : Int32 + it "should run correctly" do + val1.should be_a Int32 + val2.should be_a Int32 + val1.should eq 750 + val2.should eq 250 + end + val1 + val2 + end +end diff --git a/spec/controllers/bool_controller.cr b/spec/controllers/bool_controller.cr new file mode 100644 index 000000000..c055f2d27 --- /dev/null +++ b/spec/controllers/bool_controller.cr @@ -0,0 +1,19 @@ +class BoolController < Athena::ClassController + @[Athena::Get(path: "bool/:val")] + def self.bool(val : Bool) : Bool + it "should run correctly" do + val.should be_a Bool + val.should eq true + end + val + end + + @[Athena::Post(path: "bool")] + def self.boolPost(val : Bool) : Bool + it "should run correctly" do + val.should be_a Bool + val.should eq true + end + val + end +end diff --git a/spec/controllers/float_controller.cr b/spec/controllers/float_controller.cr new file mode 100644 index 000000000..8c34bd62d --- /dev/null +++ b/spec/controllers/float_controller.cr @@ -0,0 +1,37 @@ +class FloatController < Athena::ClassController + @[Athena::Get(path: "float32/:num")] + def self.float32(num : Float32) : Float32 + it "should run correctly" do + num.should be_a Float32 + num.should eq -2342.223_f32 + end + num + end + + @[Athena::Post(path: "float32/")] + def self.float32Post(num : Float32) : Float32 + it "should run correctly" do + num.should be_a Float32 + num.should eq -2342.223_f32 + end + num + end + + @[Athena::Get(path: "float64/:num")] + def self.float64(num : Float64) : Float64 + it "should run correctly" do + num.should be_a Float64 + num.should eq 2342.234234234223 + end + num + end + + @[Athena::Post(path: "float64")] + def self.float64Post(num : Float64) : Float64 + it "should run correctly" do + num.should be_a Float64 + num.should eq 2342.234234234223 + end + num + end +end diff --git a/spec/controllers/int_controller.cr b/spec/controllers/int_controller.cr new file mode 100644 index 000000000..93a6bf7e8 --- /dev/null +++ b/spec/controllers/int_controller.cr @@ -0,0 +1,181 @@ +class IntController < Athena::ClassController + @[Athena::Get(path: "int8/:num")] + def self.int8(num : Int8) : Int8 + it "should run correctly" do + num.should be_a Int8 + num.should eq 123 + end + num + end + + @[Athena::Post(path: "int8")] + def self.int8Post(num : Int8) : Int8 + it "should run correctly" do + num.should be_a Int8 + num.should eq 123 + end + num + end + + @[Athena::Get(path: "int16/:num")] + def self.int16(num : Int16) : Int16 + it "should run correctly" do + num.should be_a Int16 + num.should eq 456 + end + num + end + + @[Athena::Post(path: "int16/")] + def self.int16Post(num : Int16) : Int16 + it "should run correctly" do + num.should be_a Int16 + num.should eq 456 + end + num + end + + @[Athena::Get(path: "int32/:num")] + def self.int32(num : Int32) : Int32 + it "should run correctly" do + num.should be_a Int32 + num.should eq 111111 + end + num + end + + @[Athena::Post(path: "int32")] + def self.int32Post(num : Int32) : Int32 + it "should run correctly" do + num.should be_a Int32 + num.should eq 111111 + end + num + end + + @[Athena::Get(path: "int64/:num")] + def self.int64(num : Int64) : Int64 + it "should run correctly" do + num.should be_a Int64 + num.should eq 9999999999999999 + end + num + end + + @[Athena::Post(path: "int64")] + def self.int64Post(num : Int64) : Int64 + it "should run correctly" do + num.should be_a Int64 + num.should eq 9999999999999999 + end + num + end + + # @[Athena::Get(path: "int128/:num")] + # def self.int128(num : Int128) : Int128 + # it "should run correctly" do + # num.should be_a Int128 + # num.should eq 9999999999999999 + # end + # num + # end + + # @[Athena::Post(path: "int128/:num")] + # def self.int128Post(num : Int128) : Int128 + # it "should run correctly" do + # num.should be_a Int128 + # num.should eq 9999999999999999 + # end + # num + # end + + @[Athena::Get(path: "uint8/:num")] + def self.uint8(num : UInt8) : UInt8 + it "should run correctly" do + num.should be_a UInt8 + num.should eq 123 + end + num + end + + @[Athena::Post(path: "uint8/")] + def self.uint8Post(num : UInt8) : UInt8 + it "should run correctly" do + num.should be_a UInt8 + num.should eq 123 + end + num + end + + @[Athena::Get(path: "uint16/:num")] + def self.uint16(num : UInt16) : UInt16 + it "should run correctly" do + num.should be_a UInt16 + num.should eq 456 + end + num + end + + @[Athena::Post(path: "uint16")] + def self.uint1Post(num : UInt16) : UInt16 + it "should run correctly" do + num.should be_a UInt16 + num.should eq 456 + end + num + end + + @[Athena::Get(path: "uint32/:num")] + def self.uint32(num : UInt32) : UInt32 + it "should run correctly" do + num.should be_a UInt32 + num.should eq 111111 + end + num + end + + @[Athena::Post(path: "uint32")] + def self.uint32Post(num : UInt32) : UInt32 + it "should run correctly" do + num.should be_a UInt32 + num.should eq 111111 + end + num + end + + @[Athena::Get(path: "uint64/:num")] + def self.uint64(num : UInt64) : UInt64 + it "should run correctly" do + num.should be_a UInt64 + num.should eq 9999999999999999 + end + num + end + + @[Athena::Post(path: "uint64/")] + def self.uint64Post(num : UInt64) : UInt64 + it "should run correctly" do + num.should be_a UInt64 + num.should eq 9999999999999999 + end + num + end + + # @[Athena::Get(path: "uint128/:num")] + # def self.uint128(num : UInt128) : UInt128 + # it "should run correctly" do + # num.should be_a UInt128 + # num.should eq 9999999999999999 + # end + # num + # end + + # @[Athena::Post(path: "uint128")] + # def self.uint128Post(num : UInt128) : UInt128 + # it "should run correctly" do + # num.should be_a UInt128 + # num.should eq 9999999999999999 + # end + # num + # end +end diff --git a/spec/controllers/string_controller.cr b/spec/controllers/string_controller.cr new file mode 100644 index 000000000..68852146b --- /dev/null +++ b/spec/controllers/string_controller.cr @@ -0,0 +1,19 @@ +class StringController < Athena::ClassController + @[Athena::Get(path: "string/:val")] + def self.string(val : String) : String + it "should run correctly" do + val.should be_a String + val.should eq "sdfsd" + end + val + end + + @[Athena::Post(path: "string")] + def self.stringPost(val : String) : String + it "should run correctly" do + val.should be_a String + val.should eq "sdfsd" + end + val + end +end diff --git a/spec/controllers/struct_controller.cr b/spec/controllers/struct_controller.cr new file mode 100644 index 000000000..c0d6c9ded --- /dev/null +++ b/spec/controllers/struct_controller.cr @@ -0,0 +1,19 @@ +struct SController < Athena::StructController + @[Athena::Get(path: "struct/:val")] + def self.doWork(val : Int32) : Int32 + it "should run correctly" do + val.should be_a Int32 + val.should eq 123 + end + -val + end + + @[Athena::Post(path: "struct")] + def self.doWorkPost(val : Int32) : Int32 + it "should run correctly" do + val.should be_a Int32 + val.should eq 123 + end + -val + end +end diff --git a/spec/controllers/user_controller.cr b/spec/controllers/user_controller.cr new file mode 100644 index 000000000..9c9832bc7 --- /dev/null +++ b/spec/controllers/user_controller.cr @@ -0,0 +1,13 @@ +class User + include CrSerializer + + @[Assert::GreaterThan(value: 0)] + property age : Int32 = 12 + + def self.find(val) + val == 1 ? self.new : nil + end +end + +# class UserController < Athena::ClassController +# end diff --git a/spec/get_spec.cr b/spec/get_spec.cr new file mode 100644 index 000000000..d5ec39d4e --- /dev/null +++ b/spec/get_spec.cr @@ -0,0 +1,89 @@ +require "./spec_helper" + +describe Athena::Get do + describe "with an no params" do + it "works" do + CLIENT.get("/noParamsGet").body.should eq "foobar" + end + end + + describe "with two route params" do + it "works" do + CLIENT.get("/double/1000/9000").body.should eq "10000" + end + end + + describe "param conversion" do + context "Int" do + it "Int8" do + CLIENT.get("/int8/123").body.should eq "123" + end + + it "Int16" do + CLIENT.get("/int16/456").body.should eq "456" + end + + it "Int32" do + CLIENT.get("/int32/111111").body.should eq "111111" + end + + it "Int64" do + CLIENT.get("/int64/9999999999999999").body.should eq "9999999999999999" + end + + pending "Int128" do + CLIENT.get("/int128/9999999999999999999999").body.should eq "9999999999999999999999" + end + end + + context "UInt" do + it "UInt8" do + CLIENT.get("/uint8/123").body.should eq "123" + end + + it "UInt16" do + CLIENT.get("/uint16/456").body.should eq "456" + end + + it "UInt32" do + CLIENT.get("/uint32/111111").body.should eq "111111" + end + + it "UInt64" do + CLIENT.get("/uint64/9999999999999999").body.should eq "9999999999999999" + end + + pending "UInt128" do + CLIENT.get("/uint128/9999999999999999999999").body.should eq "9999999999999999999999" + end + end + + context "Float" do + it "Float32" do + CLIENT.get("/float32/-2342.223").body.should eq "-2342.223" + end + + it "Float64" do + CLIENT.get("/float64/2342.234234234223").body.should eq "2342.234234234223" + end + end + + context "Bool" do + it "Bool" do + CLIENT.get("/bool/true").body.should eq "true" + end + end + + context "String" do + it "String" do + CLIENT.get("/string/sdfsd").body.should eq "sdfsd" + end + end + + context "Struct" do + it "Struct" do + CLIENT.get("/struct/123").body.should eq "-123" + end + end + end +end diff --git a/spec/post_spec.cr b/spec/post_spec.cr new file mode 100644 index 000000000..7f9e9a54e --- /dev/null +++ b/spec/post_spec.cr @@ -0,0 +1,89 @@ +require "./spec_helper" + +describe Athena::Post do + describe "with an no params" do + it "works" do + CLIENT.post("/noParamsPost").body.should eq "foobar" + end + end + + describe "with a route param and body param" do + it "works" do + CLIENT.post("/double/750", body: "250", headers: HTTP::Headers{"content-type" => "application/json"}).body.should eq "1000" + end + end + + describe "body conversion" do + context "Int" do + it "Int8" do + CLIENT.post("/int8", body: "123", headers: HTTP::Headers{"content-type" => "application/json"}).body.should eq "123" + end + + it "Int16" do + CLIENT.post("/int16/", body: "456", headers: HTTP::Headers{"content-type" => "application/json"}).body.should eq "456" + end + + it "Int32" do + CLIENT.post("/int32/", body: "111111", headers: HTTP::Headers{"content-type" => "application/json"}).body.should eq "111111" + end + + it "Int64" do + CLIENT.post("/int64/", body: "9999999999999999", headers: HTTP::Headers{"content-type" => "application/json"}).body.should eq "9999999999999999" + end + + pending "Int128" do + CLIENT.post("/int128/", body: "9999999999999999999999", headers: HTTP::Headers{"content-type" => "application/json"}).body.should eq "9999999999999999999999" + end + end + end + + context "UInt" do + it "UInt8" do + CLIENT.post("/uint8", body: "123", headers: HTTP::Headers{"content-type" => "application/json"}).body.should eq "123" + end + + it "UInt16" do + CLIENT.post("/uint16", body: "456", headers: HTTP::Headers{"content-type" => "application/json"}).body.should eq "456" + end + + it "UInt32" do + CLIENT.post("/uint32", body: "111111", headers: HTTP::Headers{"content-type" => "application/json"}).body.should eq "111111" + end + + it "UInt64" do + CLIENT.post("/uint64/", body: "9999999999999999", headers: HTTP::Headers{"content-type" => "application/json"}).body.should eq "9999999999999999" + end + + pending "UInt128" do + CLIENT.post("/uint128/", body: "9999999999999999999999", headers: HTTP::Headers{"content-type" => "application/json"}).body.should eq "9999999999999999999999" + end + end + + context "Float" do + it "Float32" do + CLIENT.post("/float32/", body: "-2342.223", headers: HTTP::Headers{"content-type" => "application/json"}).body.should eq "-2342.223" + end + + it "Float64" do + CLIENT.post("/float64", body: "2342.234234234223", headers: HTTP::Headers{"content-type" => "application/json"}).body.should eq "2342.234234234223" + end + end + + context "Bool" do + it "Bool" do + CLIENT.post("/bool", body: "true", headers: HTTP::Headers{"content-type" => "application/json"}).body.should eq "true" + end + end + + context "String" do + it "String" do + CLIENT.post("/string", body: "sdfsd", headers: HTTP::Headers{"content-type" => "application/json"}).body.should eq "sdfsd" + end + end + + context "Struct" do + it "Struct" do + CLIENT.post("/struct", body: "123", headers: HTTP::Headers{"content-type" => "application/json"}).body.should eq "-123" + end + end +end diff --git a/spec/spec_helper.cr b/spec/spec_helper.cr new file mode 100644 index 000000000..9f6dcd572 --- /dev/null +++ b/spec/spec_helper.cr @@ -0,0 +1,8 @@ +require "spec" +require "http/client" +require "../src/athena" +require "./controllers/*" + +CLIENT = HTTP::Client.new "localhost", 8888 + +spawn Athena.run diff --git a/src/athena.cr b/src/athena.cr new file mode 100644 index 000000000..220b2eef1 --- /dev/null +++ b/src/athena.cr @@ -0,0 +1,57 @@ +require "http/server" +require "radix" +require "json" +require "CrSerializer" + +require "./route_handler" +require "./converters" +require "./types" + +macro halt(context, status_code = 200, response = "") + {{context}}.response.status_code = {{status_code}} + {{context}}.response.print {{response}} + {{context}}.response.close +end + +module Athena + # :nodoc: + module HTTP::Handler + def call_next(context : HTTP::Server::Context) + if next_handler = @next + next_handler.call(context) + end + end + end + + annotation Get; end + annotation Post; end + annotation Put; end + annotation ParamConverter; end + + abstract class ClassController; end + + abstract struct StructController; end + + abstract struct Action; end + + record RouteAction(A) < Action, action : A, path : String, requirements = {} of String => Regex + + def self.run(port : Int32 = 8888, binding : String = "0.0.0.0", ssl : OpenSSL::SSL::Context::Server? | Bool? = nil, handlers : Array(HTTP::Handler) = [Athena::RouteHandler.new]) + server : HTTP::Server = HTTP::Server.new handlers + puts "Athena is leading the way" + + unless server.each_address { |_| break true } + {% if flag?(:without_openssl) %} + server.bind_tcp(binding, port) + {% else %} + if ssl + server.bind_tls(binding, port, ssl) + else + server.bind_tcp(binding, port) + end + {% end %} + end + + server.listen + end +end diff --git a/src/converters.cr b/src/converters.cr new file mode 100644 index 000000000..aa52d2717 --- /dev/null +++ b/src/converters.cr @@ -0,0 +1,9 @@ +module Athena::Converters + class RequestBody(T) + def self.convert(body : String) : T + model : T = T.deserialize body + raise CrSerializer::Exceptions::ValidationException.new model.validator unless model.validator.valid? + model + end + end +end diff --git a/src/route_handler.cr b/src/route_handler.cr new file mode 100644 index 000000000..ad50cad62 --- /dev/null +++ b/src/route_handler.cr @@ -0,0 +1,92 @@ +module Athena + class Athena::RouteHandler + include HTTP::Handler + + @tree : Radix::Tree(Action) = Radix::Tree(Action).new + + def initialize + {% for c in Athena::ClassController.all_subclasses + Athena::StructController.all_subclasses %} + {% methods = c.class.methods.select { |m| m.annotation(Get) || m.annotation(Post) || m.annotation(Put) } %} + {% for m in methods %} + {% raise "Route action return type must be set for #{c.name}.#{m.name}" if m.return_type.stringify.empty? %} + {% if d = m.annotation(Get) %} + {% method = "GET" %} + {% route_def = d %} + {% elsif d = m.annotation(Post) %} + {% method = "POST" %} + {% route_def = d %} + {% elsif d = m.annotation(Put) %} + {% method = "PUT" %} + {% route_def = d %} + {% end %} + + {% path = "/" + method + (route_def[:path].stringify.starts_with?('/') ? route_def[:path] : "/" + route_def[:path]) %} + {% arg_types = m.args.map(&.restriction) %} + {% arg_names = m.args.map(&.name) %} + {% requirements = route_def[:requirements] %} + %proc = ->(vals : Array(String), context : HTTP::Server::Context) do + {% begin %} + {% unless m.args.empty? %} + arr = Array(Union({{arg_types.splat}})).new + {% for type, idx in arg_types %} + {% converter = m.annotation(ParamConverter) %} + {% if converter && converter[:param] == arg_names[idx] %} + arr << Athena::Converters::{{converter[:converter]}}({{converter[:type]}}).convert(vals[{{idx}}]) + {% else %} + arr << Athena::Types.convert_type(vals[{{ idx }}], {{type}}) + {% end %} + {% end %} + ->{{c.name.id}}.{{m.name.id}}({{arg_types.splat}}).call(*Tuple({{arg_types.splat}}).from(arr)) + {% else %} + ->{ {{c.name.id}}.{{m.name.id}} }.call + {% end %} + {% end %} + end + @tree.add {{path}}, RouteAction(Proc(Array(String), HTTP::Server::Context, {{m.return_type}})).new(%proc, {{path}} {% if requirements %}, {{requirements}} {% end %}) + {% end %} + {% end %} + end + + def call(context) + search_key = '/' + context.request.method + context.request.path + route = @tree.find search_key + unless route.found? + halt context, 404, %({"code": 404, "message": "No route found for '#{context.request.method} #{context.request.path}'"}) + call_next context + return + end + action : Action = route.payload + params = (context.request.path.split('/') - action.path.split('/')) + + if context.request.body && context.request.headers["Content-Type"]?.try(&.starts_with?("application/json")) + params << context.request.body.not_nil!.gets_to_end + end + + unless params.empty? + placeholders = action.path.split('/').select { |str| str.starts_with? ':' } + + placeholders.each_with_index do |p, idx| + regex : Regex? = action.requirements[p.lchop(':')]? + next if regex.nil? + unless params[idx] =~ regex + halt context, 404, %({"code": 404, "message": "No route found for '#{context.request.method} #{context.request.path}'"}) + call_next context + return + end + end + end + + response = action.action.call params, context + + if response.is_a?(String) + context.response.print response + else + context.response.print response.responds_to?(:serialize) ? response.serialize : response.to_json + end + rescue e : ArgumentError + halt context, 400, %({"code": 400, "message": "#{e.message}"}) + rescue validation_exception : CrSerializer::Exceptions::ValidationException + halt context, 400, validation_exception.to_json + end + end +end diff --git a/src/types.cr b/src/types.cr new file mode 100644 index 000000000..3cddb88b9 --- /dev/null +++ b/src/types.cr @@ -0,0 +1,51 @@ +module Athena::Types + extend self + + def convert_type(val : String, t : Int8.class) : Int8 + val.to_i8 + end + + def convert_type(val : String, t : Int16.class) : Int16 + val.to_i16 + end + + def convert_type(val : String, t : Int32.class) : Int32 + val.to_i + end + + def convert_type(val : String, t : Int64.class) : Int64 + val.to_i64 + end + + def convert_type(val : String, t : UInt8.class) : UInt8 + val.to_u8 + end + + def convert_type(val : String, t : UInt16.class) : UInt16 + val.to_u16 + end + + def convert_type(val : String, t : UInt32.class) : UInt32 + val.to_u32 + end + + def convert_type(val : String, t : UInt64.class) : UInt64 + val.to_u64 + end + + def convert_type(val : String, t : Float32.class) : Float32 + val.to_f32 + end + + def convert_type(val : String, t : Float64.class) : Float64 + val.to_f + end + + def convert_type(val : String, t : Bool.class) : Bool + val == "true" + end + + def convert_type(val : String, t : String.class) : String + val + end +end