Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ form.values # => {name: "...", bio: nil, slug: "..."}

### Resources

`Crumble::Resource` gives you RESTful handlers with sensible defaults for `index`, `show`, `create`, `update`, and `destroy`. Routing follows the class name (`CommentsResource` → `/comments`); nested paths are supported one level deep via `self.nested_path`.
`Crumble::Resource` gives you RESTful handlers with sensible defaults for `index`, `show`, `create`, `update`, and `destroy`. Routing follows the class name (`CommentsResource` → `/comments`); use the same `root_path`, `path_param`, and `nested_path` declarations as pages for custom Resource paths.

```crystal
require "css"
Expand Down Expand Up @@ -206,6 +206,7 @@ end
```

- `render SomeView` wraps the view in the configured layout and sets `Content-Type` to HTML.
- Declare path params such as `path_param id` when a Resource should handle member routes; collection paths route to `index`/`create`, and full param paths route to `show`/`update`.
- `redirect` and `redirect_back` set a `303 See Other` by default.
- Add `before` filters to short-circuit with `false` (400) or an `Int32` status code.

Expand Down
6 changes: 5 additions & 1 deletion spec/resource/before_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ require "../spec_helper"
module Crumble::Resource::BeforeSpec
class Parent < Crumble::Resource
before do
if id?
if member?
true
else
false
Expand All @@ -12,6 +12,8 @@ module Crumble::Resource::BeforeSpec
end

class Res1 < Parent
path_param id

before(:show) do
true
end
Expand All @@ -30,6 +32,8 @@ module Crumble::Resource::BeforeSpec
end

class Res2 < Parent
path_param id

def index
raise "This should not be reached!"
end
Expand Down
59 changes: 57 additions & 2 deletions spec/resource/path_matching_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,32 @@ require "../spec_helper"

module Crumble::Resource::PathMatchingSpec
class RootResource < Resource
def self.root_path
"/"
root_path "/"
end

class PageStyleResource < Resource
root_path "/accounts"
path_param account_id
path_param slug, /[a-z0-9-]+/
nested_path "posts"
nested_path "details"

def index
render "accounts index"
end

def show
render "account_id=#{account_id} slug=#{slug}"
end
end

class NestedMemberResource < Resource
root_path "/nested-members"
path_param id
nested_path "details"

def show
render "nested id=#{id}"
end
end

Expand All @@ -15,5 +39,36 @@ module Crumble::Resource::PathMatchingSpec
it "should not match on any other path ending on /" do
RootResource.match("/test/").should be_falsey
end

it "does not match member paths without declared params" do
RootResource.match("/1").should be_falsey
end
end

describe "PageStyleResource.match" do
it "supports the same path matching declarations as pages" do
PageStyleResource.match("/accounts").should be_truthy
PageStyleResource.uri_path(account_id: 123, slug: "hello-world").should eq("/accounts/123/hello-world/posts/details")
PageStyleResource.match("/accounts/123/hello-world/posts/details").should be_truthy
PageStyleResource.match("/accounts/123/hello_world/posts/details").should be_falsey
end

it "exposes declared path params while handling requests" do
response = String.build do |io|
ctx = Crumble::Server::TestRequestContext.new(response_io: io, resource: PageStyleResource.uri_path(account_id: 123, slug: "hello-world"))
PageStyleResource.handle(ctx).should eq(true)
ctx.response.flush
end

response.should contain("account_id=123 slug=hello-world")
end
end

describe "NestedMemberResource.match" do
it "does not match the parent member path without the nested component" do
NestedMemberResource.match("/nested-members").should be_truthy
NestedMemberResource.match(NestedMemberResource.uri_path(id: 7)).should be_truthy
NestedMemberResource.match("/nested-members/7").should be_falsey
end
end
end
4 changes: 1 addition & 3 deletions spec/resource/redirect_back_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@ require "../spec_helper"

module Crumble::Resource::RedirectBackSpec
class HomeResource < Resource
def self.root_path
"/"
end
root_path "/"
end

class MyResource < Resource
Expand Down
2 changes: 2 additions & 0 deletions spec/resource/redirect_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ require "../spec_helper"

module Crumble::Resource::RedirectSpec
class MyResource < Resource
path_param id

def create
redirect self.class.uri_path(1)
end
Expand Down
2 changes: 2 additions & 0 deletions spec/resource/routing_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ require "../spec_helper"

module Crumble::Resource::RoutingSpec
class MyResource < Resource
path_param id

def index
render "Index!"
end
Expand Down
9 changes: 3 additions & 6 deletions spec/server/request_dispatcher_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,8 @@ end

module Crumble::Server::RequestDispatcherResourceSpec
class WidgetResource < Crumble::Resource
def self.root_path
"/root-handler-resource"
end
root_path "/root-handler-resource"
path_param id

def index
ctx.response.print "resource index"
Expand All @@ -64,9 +63,7 @@ module Crumble::Server::RequestDispatcherResourceSpec
end

class SessionResource < Crumble::Resource
def self.root_path
"/root-handler-session"
end
root_path "/root-handler-session"

def index
current = ctx.session.blah || 0
Expand Down
4 changes: 4 additions & 0 deletions src/path_matching.cr
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ module Crumble::PathMatching
end

def self.uri_path(**params)
_path_matching_uri_path(**params)
end

def self._path_matching_uri_path(**params)
param_values = {} of Symbol => String
params.each do |key, value|
param_values[key] = value.to_s
Expand Down
64 changes: 31 additions & 33 deletions src/resource/resource.cr
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
require "../server/view_handler"
require "../path_matching"

abstract class Crumble::Resource
include Crumble::Server::ViewHandler
include Crumble::PathMatching

def self._path_matching_root_suffix : String
"Resource"
end

macro before(&blk)
before(:index, :create, :show, :update, :destroy) {{blk}}
Expand Down Expand Up @@ -44,15 +50,15 @@ abstract class Crumble::Resource

case ctx.request.method
when "GET"
if instance.id? && instance.top_level?
if instance.member?
before_action_handling(instance, :show)
instance.show
else
before_action_handling(instance, :index)
instance.index
end
when "POST"
if instance.id? && instance.top_level?
if instance.member?
before_action_handling(instance, :update)
instance.update
else
Expand All @@ -66,37 +72,33 @@ abstract class Crumble::Resource
return true
end

def self.match(path)
uri_path_matcher.match(path)
end

def self.root_path
"/" + self.name.chomp("Resource").gsub("::", "/").underscore
def self.uri_path(id = nil)
return _root_path unless id
raise ArgumentError.new("Cannot build a member path for #{self} without path params") if _path_parts.empty?
_path_matching_uri_path(id: id)
end

def self.root_path(id)
"#{root_path}/#{id}"
end
def self.uri_path_matcher
Comment thread
sbsoftware marked this conversation as resolved.
if path_param?
collection_pattern = _root_path_segment_patterns.empty? ? "/" : "/" + _root_path_segment_patterns.join("/")
member_pattern = "/" + (_root_path_segment_patterns + _path_parts.map(&.segment_pattern)).join("/")

def self.nested_path
""
if collection_pattern == "/"
Regex.new("^(?:/|#{member_pattern}/?)$")
else
Regex.new("^(?:#{collection_pattern}|#{member_pattern})/?$")
end
else
previous_def
end
end

def self.uri_path(id = nil)
path = root_path
if id
path += "/#{id}"
path += nested_path
end
path
def self.path_param?
_path_parts.any? { |part| part.is_a?(Crumble::PathMatching::ParamPathPart) }
end

def self.uri_path_matcher
if nested_path.empty?
/^#{root_path}(\/|\/(\d+))?$/
else
/^#{root_path}(\/|\/(\d+)(#{nested_path})?)?$/
end
def self._root_path_segment_patterns
_root_path.split('/').reject(&.empty?).map { |seg| Regex.escape(seg) }
end

def initialize(@request_ctx); end
Expand Down Expand Up @@ -185,16 +187,12 @@ abstract class Crumble::Resource
ctx.response.print "Not Found"
end

def id?
self.class.match(ctx.request.path).try { |m| m[2]?.try(&.to_i64) }
end

def id
id?.not_nil!
def member?
path_params.size > 0
end

def nested?
self.class.nested_path.size > 0
self.class._path_parts.any? { |part| part.is_a?(Crumble::PathMatching::NestedPathPart) }
end

def top_level?
Expand Down