mirror of
				https://gitea.invidious.io/iv-org/shard-kemal.git
				synced 2024-08-15 00:53:36 +00:00 
			
		
		
		
	Refactor class level DSL with macros to convert blocks to instance-scoped methods
This commit is contained in:
		
							parent
							
								
									53fa65f964
								
							
						
					
					
						commit
						ad91a22789
					
				
					 6 changed files with 179 additions and 51 deletions
				
			
		
							
								
								
									
										17
									
								
								samples/app_squared.cr
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								samples/app_squared.cr
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,17 @@ | ||||||
|  | require "../src/kemal/base" | ||||||
|  | 
 | ||||||
|  | class MyApp < Kemal::Application | ||||||
|  |   get "/" do |env| | ||||||
|  |     "Hello Kemal!" | ||||||
|  |   end | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | class OtherApp < Kemal::Application | ||||||
|  |   get "/" do |env| | ||||||
|  |     "Hello World!" | ||||||
|  |   end | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | spawn { MyApp.run(3002) } | ||||||
|  | 
 | ||||||
|  | OtherApp.run(3001) | ||||||
|  | @ -8,6 +8,10 @@ private class MyApp < Kemal::Application | ||||||
|   get "/route2" do |env| |   get "/route2" do |env| | ||||||
|     "Route 2" |     "Route 2" | ||||||
|   end |   end | ||||||
|  | 
 | ||||||
|  |   get "/file" do |env| | ||||||
|  |     send_file env, "Serdar".to_slice | ||||||
|  |   end | ||||||
| end | end | ||||||
| 
 | 
 | ||||||
| describe MyApp do | describe MyApp do | ||||||
|  | @ -24,4 +28,23 @@ describe MyApp do | ||||||
|       end |       end | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
|  | 
 | ||||||
|  |   it "sends file with binary stream" do | ||||||
|  |     request = HTTP::Request.new("GET", "/file") | ||||||
|  |     response = call_request_on_app(MyApp.new, request) | ||||||
|  |     response.status_code.should eq(200) | ||||||
|  |     response.headers["Content-Type"].should eq("application/octet-stream") | ||||||
|  |     response.headers["Content-Length"].should eq("6") | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   it "responds to delayed route" do | ||||||
|  |     app = MyApp.new | ||||||
|  |     app.setup | ||||||
|  |     app.get "/delayed" do |env| | ||||||
|  |       "Happy addition!" | ||||||
|  |     end | ||||||
|  |     request = HTTP::Request.new("GET", "/delayed") | ||||||
|  |     client_response = call_request_on_app(app, request) | ||||||
|  |     client_response.body.should eq("Happy addition!") | ||||||
|  |   end | ||||||
| end | end | ||||||
|  |  | ||||||
|  | @ -42,7 +42,7 @@ describe "Macros" do | ||||||
|       client_response.body.should eq("world") |       client_response.body.should eq("world") | ||||||
| 
 | 
 | ||||||
|       app.get "/breaking" do |env| |       app.get "/breaking" do |env| | ||||||
|         halt env, 404, "hello" |         Kemal::Macros.halt env, 404, "hello" | ||||||
|         "world" |         "world" | ||||||
|       end |       end | ||||||
|       request = HTTP::Request.new("GET", "/breaking") |       request = HTTP::Request.new("GET", "/breaking") | ||||||
|  | @ -54,7 +54,7 @@ describe "Macros" do | ||||||
|     it "can break block with halt macro using default values" do |     it "can break block with halt macro using default values" do | ||||||
|       app = Kemal::Base.new |       app = Kemal::Base.new | ||||||
|       app.get "/" do |env| |       app.get "/" do |env| | ||||||
|         halt env |         Kemal::Macros.halt env | ||||||
|         "world" |         "world" | ||||||
|       end |       end | ||||||
|       request = HTTP::Request.new("GET", "/") |       request = HTTP::Request.new("GET", "/") | ||||||
|  | @ -69,7 +69,7 @@ describe "Macros" do | ||||||
|       app = Kemal::Base.new |       app = Kemal::Base.new | ||||||
|       app.get "/headers" do |env| |       app.get "/headers" do |env| | ||||||
|         env.response.headers.add "Content-Type", "image/png" |         env.response.headers.add "Content-Type", "image/png" | ||||||
|         headers env, { |         app.headers env, { | ||||||
|           "Access-Control-Allow-Origin" => "*", |           "Access-Control-Allow-Origin" => "*", | ||||||
|           "Content-Type"                => "text/plain", |           "Content-Type"                => "text/plain", | ||||||
|         } |         } | ||||||
|  | @ -85,7 +85,7 @@ describe "Macros" do | ||||||
|     it "sends file with given path and default mime-type" do |     it "sends file with given path and default mime-type" do | ||||||
|       app = Kemal::Base.new |       app = Kemal::Base.new | ||||||
|       app.get "/" do |env| |       app.get "/" do |env| | ||||||
|         send_file env, "./spec/asset/hello.ecr" |         app.send_file env, "./spec/asset/hello.ecr" | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|       request = HTTP::Request.new("GET", "/") |       request = HTTP::Request.new("GET", "/") | ||||||
|  | @ -98,7 +98,7 @@ describe "Macros" do | ||||||
|     it "sends file with given path and given mime-type" do |     it "sends file with given path and given mime-type" do | ||||||
|       app = Kemal::Base.new |       app = Kemal::Base.new | ||||||
|       app.get "/" do |env| |       app.get "/" do |env| | ||||||
|         send_file env, "./spec/asset/hello.ecr", "image/jpeg" |         app.send_file env, "./spec/asset/hello.ecr", "image/jpeg" | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|       request = HTTP::Request.new("GET", "/") |       request = HTTP::Request.new("GET", "/") | ||||||
|  | @ -111,7 +111,7 @@ describe "Macros" do | ||||||
|     it "sends file with binary stream" do |     it "sends file with binary stream" do | ||||||
|       app = Kemal::Base.new |       app = Kemal::Base.new | ||||||
|       app.get "/" do |env| |       app.get "/" do |env| | ||||||
|         send_file env, "Serdar".to_slice |         app.send_file env, "Serdar".to_slice | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|       request = HTTP::Request.new("GET", "/") |       request = HTTP::Request.new("GET", "/") | ||||||
|  |  | ||||||
|  | @ -14,7 +14,7 @@ class Kemal::Application < Kemal::Base | ||||||
|     super |     super | ||||||
| 
 | 
 | ||||||
|     unless error_handlers.has_key?(404) |     unless error_handlers.has_key?(404) | ||||||
|       error 404 do |env| |       self.error 404 do |env| | ||||||
|         render_404 |         render_404 | ||||||
|       end |       end | ||||||
|     end |     end | ||||||
|  | @ -22,7 +22,7 @@ class Kemal::Application < Kemal::Base | ||||||
|     # Test environment doesn't need to have signal trap, built-in images, and logging. |     # Test environment doesn't need to have signal trap, built-in images, and logging. | ||||||
|     unless @config.env == "test" |     unless @config.env == "test" | ||||||
|       # This route serves the built-in images for not_found and exceptions. |       # This route serves the built-in images for not_found and exceptions. | ||||||
|       get "/__kemal__/:image" do |env| |       self.get "/__kemal__/:image" do |env| | ||||||
|         image = env.params.url["image"] |         image = env.params.url["image"] | ||||||
|         file_path = File.expand_path("lib/kemal/images/#{image}", Dir.current) |         file_path = File.expand_path("lib/kemal/images/#{image}", Dir.current) | ||||||
|         if File.exists? file_path |         if File.exists? file_path | ||||||
|  |  | ||||||
|  | @ -13,7 +13,6 @@ class Kemal::Base | ||||||
|   include Macros |   include Macros | ||||||
|   include Base::DSL |   include Base::DSL | ||||||
|   include Base::Builder |   include Base::Builder | ||||||
|   extend Base::ClassDSL |  | ||||||
| 
 | 
 | ||||||
|   # :nodoc: |   # :nodoc: | ||||||
|   getter route_handler = Kemal::RouteHandler.new |   getter route_handler = Kemal::RouteHandler.new | ||||||
|  | @ -41,9 +40,7 @@ class Kemal::Base | ||||||
| 
 | 
 | ||||||
|   # Overload of self.run with the default startup logging |   # Overload of self.run with the default startup logging | ||||||
|   def run(port : Int32? = nil) |   def run(port : Int32? = nil) | ||||||
|     run port do |     run(port) { } | ||||||
|       log "[#{config.env}] Kemal is ready to lead at #{config.scheme}://#{config.host_binding}:#{config.port}" |  | ||||||
|     end |  | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   # The command to run a `Kemal` application. |   # The command to run a `Kemal` application. | ||||||
|  |  | ||||||
|  | @ -1,31 +1,42 @@ | ||||||
| class Kemal::Base | class Kemal::Base | ||||||
|  |   private CUSTOM_METHODS_REGISTRY = {} of _ => _ | ||||||
|  | 
 | ||||||
|  |   macro inherited | ||||||
|  |     {% CUSTOM_METHODS_REGISTRY[@type] = { | ||||||
|  |          handlers: [] of _, | ||||||
|  |          ws:       [] of _, | ||||||
|  |          error:    [] of _, | ||||||
|  |          filters:  [] of _, | ||||||
|  |        } %} | ||||||
|  | 
 | ||||||
|  |     include MacroDSL | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|   module DSL |   module DSL | ||||||
|     HTTP_METHODS   = %w(get post put patch delete options) |     HTTP_METHODS   = %w(get post put patch delete options) | ||||||
|     FILTER_METHODS = %w(get post put patch delete options all) |     FILTER_METHODS = %w(get post put patch delete options all) | ||||||
| 
 | 
 | ||||||
|     macro included |  | ||||||
|       # :nodoc: |  | ||||||
|       DEFAULT_HANDLERS = [] of {String, String, (HTTP::Server::Context -> String)} |  | ||||||
|       # :nodoc: |  | ||||||
|       WEBSOCKET_HANDLERS = [] of {String, (HTTP::WebSocket, HTTP::Server::Context -> Void)} |  | ||||||
|       # :nodoc: |  | ||||||
|       DEFAULT_ERROR_HANDLERS = [] of {Int32, (HTTP::Server::Context, Exception -> String)} |  | ||||||
|       # :nodoc: |  | ||||||
|       DEFAULT_FILTERS = [] of {Symbol, String, String, (HTTP::Server::Context -> String)} |  | ||||||
|     end |  | ||||||
| 
 |  | ||||||
|     {% for method in HTTP_METHODS %} |     {% for method in HTTP_METHODS %} | ||||||
|  |       # Add a `{{method.id.upcase}}` handler. | ||||||
|  |       # | ||||||
|  |       # The block receives an `HTTP::Server::Context` as argument. | ||||||
|       def {{method.id}}(path, &block : HTTP::Server::Context -> _) |       def {{method.id}}(path, &block : HTTP::Server::Context -> _) | ||||||
|         raise Kemal::Exceptions::InvalidPathStartException.new({{method}}, path) unless Kemal::Utils.path_starts_with_slash?(path) |         raise Kemal::Exceptions::InvalidPathStartException.new({{method}}, path) unless path.starts_with?("/") | ||||||
|         route_handler.add_route({{method}}.upcase, path, &block) |         route_handler.add_route({{method}}.upcase, path, &block) | ||||||
|       end |       end | ||||||
|     {% end %} |     {% end %} | ||||||
| 
 | 
 | ||||||
|  |     # Add a webservice handler. | ||||||
|  |     # | ||||||
|  |     # The block receives `HTTP::WebSocket` and `HTTP::Server::Context` as arguments. | ||||||
|     def ws(path, &block : HTTP::WebSocket, HTTP::Server::Context -> Void) |     def ws(path, &block : HTTP::WebSocket, HTTP::Server::Context -> Void) | ||||||
|       raise Kemal::Exceptions::InvalidPathStartException.new("ws", path) unless Kemal::Utils.path_starts_with_slash?(path) |       raise Kemal::Exceptions::InvalidPathStartException.new("ws", path) unless path.starts_with?("/") | ||||||
|       websocket_handler.add_route path, &block |       websocket_handler.add_route path, &block | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|  |     # Add an error handler for *status_code*. | ||||||
|  |     # | ||||||
|  |     # The block receives `HTTP::Server::Context` and `Exception` as arguments. | ||||||
|     def error(status_code, &block : HTTP::Server::Context, Exception -> _) |     def error(status_code, &block : HTTP::Server::Context, Exception -> _) | ||||||
|       add_error_handler status_code, &block |       add_error_handler status_code, &block | ||||||
|     end |     end | ||||||
|  | @ -35,57 +46,137 @@ class Kemal::Base | ||||||
|     #  - after_all, after_get, after_post, after_put, after_patch, after_delete, after_options |     #  - after_all, after_get, after_post, after_put, after_patch, after_delete, after_options | ||||||
|     {% for type in ["before", "after"] %} |     {% for type in ["before", "after"] %} | ||||||
|       {% for method in FILTER_METHODS %} |       {% for method in FILTER_METHODS %} | ||||||
|  |         # Add a filter for this class that runs {{type.id}} each `{{method.id.upcase}}` request (optionally limited to a specific *path*). | ||||||
|  |         # | ||||||
|  |         # The block receives an `HTTP::Server::Context` as argument. | ||||||
|         def {{type.id}}_{{method.id}}(path = "*", &block : HTTP::Server::Context -> _) |         def {{type.id}}_{{method.id}}(path = "*", &block : HTTP::Server::Context -> _) | ||||||
|           filter_handler.{{type.id}}({{method}}.upcase, path, &block) |           filter_handler.{{type.id}}({{method}}.upcase, path, &block) | ||||||
|         end |         end | ||||||
|       {% end %} |       {% end %} | ||||||
|     {% end %} |     {% end %} | ||||||
| 
 | 
 | ||||||
|     private def initialize_defaults |     private macro initialize_defaults | ||||||
|       DEFAULT_HANDLERS.each do |method, path, block| |       {% if CUSTOM_METHODS_REGISTRY[@type] %} | ||||||
|         route_handler.add_route(method.upcase, path, &block) |       {% for handler in CUSTOM_METHODS_REGISTRY[@type][:handlers] %} | ||||||
|       end |       self.{{handler[0].id}}({{handler[1]}}) do |context| | ||||||
| 
 |         {{handler[2].id}}(context) | ||||||
|       WEBSOCKET_HANDLERS.each do |path, block| |  | ||||||
|         ws(path, &block) |  | ||||||
|       end |  | ||||||
| 
 |  | ||||||
|       DEFAULT_ERROR_HANDLERS.each do |status_code, block| |  | ||||||
|         add_error_handler status_code, &block |  | ||||||
|       end |  | ||||||
| 
 |  | ||||||
|       DEFAULT_FILTERS.each do |type, method, path, block| |  | ||||||
|         if type == :before |  | ||||||
|           filter_handler.before(method, path, &block) |  | ||||||
|         else |  | ||||||
|           filter_handler.after(method, path, &block) |  | ||||||
|         end |  | ||||||
|       end |  | ||||||
|     end |  | ||||||
|   end |  | ||||||
| 
 |  | ||||||
|   module ClassDSL |  | ||||||
|     {% for method in DSL::HTTP_METHODS %} |  | ||||||
|       def {{method.id}}(path, &block : HTTP::Server::Context -> _) |  | ||||||
|         DEFAULT_HANDLERS << { {{method}}, path, block } |  | ||||||
|       end |       end | ||||||
|       {% end %} |       {% end %} | ||||||
| 
 | 
 | ||||||
|     def ws(path, &block : HTTP::WebSocket, HTTP::Server::Context -> Void) |       {% for ws in CUSTOM_METHODS_REGISTRY[@type][:ws] %} | ||||||
|       WEBSOCKET_HANDLERS << {path, block} |       self.ws({{handler[0]}}) do |websocket, context| | ||||||
|  |         {{handler[1].id}}(websocket, context) | ||||||
|  |       end | ||||||
|  |       {% end %} | ||||||
|  | 
 | ||||||
|  |       {% for ws in CUSTOM_METHODS_REGISTRY[@type][:error] %} | ||||||
|  |       self.add_error_handler({{handler[0]}}) do |context| | ||||||
|  |         {{handler[1].id}}(context) | ||||||
|  |       end | ||||||
|  |       {% end %} | ||||||
|  | 
 | ||||||
|  |       {% for filter in CUSTOM_METHODS_REGISTRY[@type][:filters] %} | ||||||
|  |         filter_handler.{{filter[0]}}({{filter[1]}}, {{filter[2]}}) do |context| | ||||||
|  |           {{filter[3]}}(context) | ||||||
|  |         end | ||||||
|  |       {% end %} | ||||||
|  |       {% end %} | ||||||
|  |     end | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|     def error(status_code, &block : HTTP::Server::Context, Exception -> _) |   module MacroDSL | ||||||
|       DEFAULT_ERROR_HANDLERS << {status_code, block} |     {% for method in DSL::HTTP_METHODS %} | ||||||
|  |       # Define a `{{method.id.upcase}}` handler for this class. | ||||||
|  |       # | ||||||
|  |       # It will be initialized in every instance. | ||||||
|  |       # The block receives an `HTTP::Server::Context` as argument and is scoped to the instance. | ||||||
|  |       # | ||||||
|  |       # Example: | ||||||
|  |       # ``` | ||||||
|  |       # class MyClass < Kemal::Base | ||||||
|  |       #   {{method.id}}("/route") do |context| | ||||||
|  |       #     # ... | ||||||
|  |       #   end | ||||||
|  |       # end | ||||||
|  |       # ``` | ||||||
|  |       # NOTE: This macro *must* be called from class scope as it expands to a custom method definition. | ||||||
|  |       macro {{method.id}}(path, &block) | ||||||
|  |         \{% raise "invalid path start for {{method.id}}: path must start with \"/\"" unless path.starts_with?("/") %} | ||||||
|  |         \{% method_name = "__{{method.id}}_#{path.id.gsub(/[^a-zA-Z0-9]/,"_").gsub(/__+/, "_").gsub(/\A_|_\z/, "")}_#{CUSTOM_METHODS_REGISTRY[@type][:handlers].size}" %} | ||||||
|  |         def \{{method_name.id}}(\{{block.args[0].id}}) | ||||||
|  |           \{{block.body}} | ||||||
|  |         end | ||||||
|  |         \{% CUSTOM_METHODS_REGISTRY[@type][:handlers] << { {{method}}, path, method_name } %} | ||||||
|  |       end | ||||||
|  |     {% end %} | ||||||
|  | 
 | ||||||
|  |     # Define a webservice handler for this class. | ||||||
|  |     # | ||||||
|  |     # It will be initialized in every instance. | ||||||
|  |     # The block receives `HTTP::WebSocket` and `HTTP::Server::Context` as arguments and is scoped to the instance. | ||||||
|  |     # | ||||||
|  |     # Example: | ||||||
|  |     # ``` | ||||||
|  |     # class MyClass < Kemal::Base | ||||||
|  |     #   ws("/wsroute") do |context| | ||||||
|  |     #     # ... | ||||||
|  |     #   end | ||||||
|  |     # end | ||||||
|  |     # ``` | ||||||
|  |     # NOTE: This macro *must* be called from class scope as it expands to a custom method definition. | ||||||
|  |     macro ws(path, &block) | ||||||
|  |         \{% raise "invalid path start for webservice: path must start with \"/\"" unless path.starts_with?("/") %} | ||||||
|  |       \{% method_name = "__ws_#{path.id.gsub(/[^a-zA-Z0-9]/,"_").gsub(/__+/, "_").gsub(/\A_|_\z/, "")}_#{CUSTOM_METHODS_REGISTRY[@type][:ws].size}" %} | ||||||
|  |       def \{{method_name.id}}(\{{block.args[0].id}}, \{{block.args[1].id}}) | ||||||
|  |         \{{block.body}} | ||||||
|  |       end | ||||||
|  |       \{% CUSTOM_METHODS_REGISTRY[@type][:ws] << { path, method_name } %} | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     # All the helper methods available are: |     # Define an error handler for this class. | ||||||
|     #  - before_all, before_get, before_post, before_put, before_patch, before_delete, before_options |     # | ||||||
|     #  - after_all, after_get, after_post, after_put, after_patch, after_delete, after_options |     # It will be initialized in every instance. | ||||||
|     {% for type in [:before, :after] %} |     # The block receives `HTTP::Server::Context` and `Exception` as arguments and is scoped to the instance. | ||||||
|  |     # | ||||||
|  |     # Example: | ||||||
|  |     # ``` | ||||||
|  |     # class MyClass < Kemal::Base | ||||||
|  |     #   error(403) do |context| | ||||||
|  |     #     # ... | ||||||
|  |     #   end | ||||||
|  |     # end | ||||||
|  |     # ``` | ||||||
|  |     # NOTE: This macro *must* be called from class scope as it expands to a custom method definition. | ||||||
|  |     macro error(status_code) | ||||||
|  |       \{% method_name = "__error_#{status_code}_#{CUSTOM_METHODS_REGISTRY[@type][:error].size}" %} | ||||||
|  |       def \{{method_name.id}}(\{{block.args[0].id}}) | ||||||
|  |         \{{block.body}} | ||||||
|  |       end | ||||||
|  |       \{% CUSTOM_METHODS_REGISTRY[@type][:error] << { status_code, method_name } %} | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     {% for type in ["before", "after"] %} | ||||||
|       {% for method in DSL::FILTER_METHODS %} |       {% for method in DSL::FILTER_METHODS %} | ||||||
|         def {{type.id}}_{{method.id}}(path = "*", &block : HTTP::Server::Context -> _) |         # Define a filter for this class that runs {{type.id}} each `{{method.id.upcase}}` request (optionally limited to a specific *path*). | ||||||
|           DEFAULT_FILTERS << { {{type}}, {{method}}, path, block } |         # | ||||||
|  |         # The filter will be initialized in every instance of this class. | ||||||
|  |         # The block receives an `HTTP::Context` as argument and is scoped to the instance. | ||||||
|  |         # | ||||||
|  |         # Example: | ||||||
|  |         # ``` | ||||||
|  |         # class MyClass < Kemal::Base | ||||||
|  |         #   {{type.id}}_{{method.id}}("/route") do |context| | ||||||
|  |         #     # ... | ||||||
|  |         #   end | ||||||
|  |         # end | ||||||
|  |         # ``` | ||||||
|  |         # NOTE: This macro *must* be called from class scope as it expands to a custom method definition. | ||||||
|  |         macro {{type.id}}_{{method.id}}(path = "*", &block) | ||||||
|  |           \{% method_name = "__{{type.id}}_{{method.id}}_#{path.id.gsub(/[^a-zA-Z0-9]/,"_").gsub(/__+/, "_").gsub(/\A_|_\z/, "")}_#{CUSTOM_METHODS_REGISTRY[@type][:handlers].size}" %} | ||||||
|  |           def \{{method_name.id}}(\{{block.args[0].id}}) | ||||||
|  |             \{{block.body}} | ||||||
|  |           end | ||||||
|  |           \{% CUSTOM_METHODS_REGISTRY[@type][:fitlers] << { {{type}}, {{method}}, path, method_name } %} | ||||||
|         end |         end | ||||||
|       {% end %} |       {% end %} | ||||||
|     {% end %} |     {% end %} | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue