require "json" require "./handler" private macro define_method_calls(hash) case namespace {% for namespace, methods in hash %} when "{{ namespace.id }}" case method {% for method in methods %} when "{{ method }}" do_{{ namespace.id }}_{{ method }}(context, get_method_args( context.request.query_params, "{{ namespace.id }}", "{{ method.id }}" )) {% end %} else raise "unknown method" end {% end %} else raise "unknown namespace" end end module Guff class APIHandler < Handler API = { "post": { "get_posts": { text: "Get posts matching query.", args: { "q": { text: "Search string.", type: :text, required: false, default: "", }, "page": { text: "Page number", type: :int, required: false, default: "1", }, "tags": { text: "Comma-separated list of tags (union)", type: :tags, required: false, default: "1", }, "sort": { text: "Sort order of results", type: :sort, required: false, default: "date,desc", }, }, }, }, "test": { "version": { text: "Get version", }, "get_posts": { text: "Test get posts", }, "error": { text: "Test error response", }, } } TYPE_CHECKS = { text: /.*/, int: /^\d+$/, tags: /^[a-z0-9_,-]+$/, sort: /^[a-z0-9_]+,(?:asc|desc)$/, } PATH_RE = %r{ ^/api (?: # method call (?: / (?[a-z0-9_-]+) / (?[a-z0-9_]+) ) | # index.html /(?:index(?:\.html|)|) | # implicit index (no trailing slash) ) $ }mx CONTENT_TYPES = { "development": "text/html; charset=utf-8", "production": "application/json; charset=utf8", } private def get_content_type CONTENT_TYPES[@model.config["environment"]] end private def get_method_args( params : HTTP::Params, namespace : String, method : String ) return {} of String => String unless ( API[namespace]? && API[namespace][method] && API[namespace][method][:args]? ) # get method args args = API[namespace][method][:args] as \ Hash(String, Hash(Symbol, String | Symbol | Bool)) args.keys.reduce({} of String => String) do |r, arg_name| arg_data = args[arg_name] as Hash(Symbol, String|Symbol|Bool) # check for required parameter if arg_data[:required] && !params.has_key?(arg_name) raise "missing required parameter: %s" % [arg_name] end # get value val = params.fetch(arg_name, arg_data[:default] as String) # check value if !TYPE_CHECKS[arg_data[:type]].match(val) raise "invalid parameter format: %s" % [arg_name] end # add value to result r[arg_name] = val # return result r end end def call(context : HTTP::Server::Context) if md = (context.request.path || "").match(PATH_RE) if md["namespace"]? # method call do_call(context, md["namespace"], md["method"]) else # api index do_docs(context) end else call_next(context) end end private def do_call( context : HTTP::Server::Context, namespace : String, method : String ) context.response.content_type = get_content_type # method call json = begin # equivalent to send("do_#{namespace}_#{method}".intern, context) define_method_calls({ post: [ get_posts, ], test: [ version, get_posts, error, ], }) rescue e { "error": e.to_s }.to_json end # send body context.response.puts json end private def do_docs(context : HTTP::Server::Context) page = HTMLPageView.new( "API Documentation", "

API Documentation

" ) context.response.content_type = page.content_type context.response.puts page end ################ # post methods # ################ private def do_post_get_posts( context : HTTP::Server::Context, args : Hash(String, String) ) [{foo: "bar"}, {foo: "asdf"}].to_json end ################ # test methods # ################ private def do_test_version( context : HTTP::Server::Context, args : Hash(String, String) ) {version: Guff::VERSION}.to_json end private def do_test_get_posts( context : HTTP::Server::Context, args : Hash(String, String) ) [{foo: "bar"}, {foo: "asdf"}].to_json end private def do_test_error( context : HTTP::Server::Context, args : Hash(String, String) ) raise "some random error" end end end