I’m trying to DRY up some API calls I have in Kemal which are very similar. For example, something like this:
get "/api/posts" do |env|
env.response.content_type = "application/json"
env.response.headers["Cache-Control"] = "No-Store"
if env.request.headers.has_key?("X-API-KEY")
if env.request.headers["X-API-KEY"] == ENV["API_KEY"]
data_provider.posts.to_json # This is the only line which differs
else
halt env, status_code: 401, response: api_unauthorized_error_json
end
else
halt env, status_code: 400, response: api_missing_key_error_json
end
end
This pattern works wll, though there’s a lot of repeated code. I tried doing this:
def api_get(env : HTTP::Server::Context, data_proc : Proc(String))
env.response.content_type = "application/json"
env.response.headers["Cache-Control"] = "No-Store"
if env.request.headers.has_key?("X-API-KEY")
if env.request.headers["X-API-KEY"] == ENV["API_KEY"]
data_proc.call
else
halt env, status_code: 401, response: api_unauthorized_error_json
end
else
halt env, status_code: 400, response: api_missing_key_error_json
end
end
get "/api/posts" do |env|
api_get(env, -> { data_provider.posts.to_json })
end
However, that returns this error:
There was a problem expanding macro 'halt'
Code in src/xxx.cr:51:7
51 | halt env, status_code: 401, response: api_unauthorized_error_json
^
Called macro defined in lib/kemal/src/kemal/helpers/macros.cr:80:1
80 | macro halt(env, status_code = 200, response = "")
Which expanded to:
> 2 | env.response.print api_unauthorized_error_json
> 3 | env.response.close
> 4 | next
^
Error: invalid next
Is it not possible what I’m try to do, or am I doing something wrong?
I think the problem is that the halt
macro expands to what you’re seeing in the error output, which includes next
. Normally when you use halt
directly within the route it’s fine since next
is what you use to exit a block early. However since you’re using it within your api_get
method, next
is not valid as it should be using return
.
Probably could do something like this instead:
def api_get(env : HTTP::Server::Context, &)
env.response.content_type = "application/json"
env.response.headers["Cache-Control"] = "No-Store"
halt env, status_code: 400, response: api_missing_key_error_json unless env.request.headers.has_key?("X-API-KEY")
halt env, status_code: 401, response: api_unauthorized_error_json unless env.request.headers["X-API-KEY"] == ENV["API_KEY"]
yield
end
get "/api/posts" do |env|
api_get env do
data_provider.posts.to_json
end
end
This way the halt
macro will still be invoked in the context of a block, not a method.
Another option would be to checkout other frameworks that have better ways to handle abstractions like this
.
EDIT: Wouldn’t https://kemalcr.com/guide/#filters make more sense for this kinda stuff, or better yet https://kemalcr.com/guide/#middleware.
1 Like
I looked at the source for that macro and wondered if next
was being used to exit the block.
I thought about unraveling the macro in my api_get
function and replacing next
with return
too. It makes it a little more verbose, though still more DRY than repeating that pattern for many api get calls.
It’s interesting you used a block parameter there. I used to use that convention often in Ruby, didn’t really think about using it here in Crystal.
Thanks!
EDIT: Wouldn’t Kemal - Guide 1 make more sense for this kinda stuff, or better yet Kemal - Guide 1.
Yeah, actually the conditional-middleware-execution makes a lot of sense for my use case, since I have a mix of html routes and json api routes. Thanks for pointing that out!