Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement a default way to handle invalid URLs #5

Open
ILikePizza555 opened this issue Mar 15, 2019 · 3 comments
Open

Implement a default way to handle invalid URLs #5

ILikePizza555 opened this issue Mar 15, 2019 · 3 comments
Labels
enhancement New feature or request help wanted Extra attention is needed Important

Comments

@ILikePizza555
Copy link
Owner

ILikePizza555 commented Mar 15, 2019

bite is currently just a normal operator that operates on Observable<Context> and filters out all requests that don't match the HTTP method and path pattern. However, no mechanism currently exists to obtain requests that haven't matched any filters.

This is an issue because as it stands, Snakey has no way to return 404s on requests which do not match any url patterns. The current behavior hangs the connection indefinitely.

I like bite being a simple functional operator, and how it is currently used, however, it may be the case that bites api and the implementation of applySnakes might need to be adjusted.

Ideally the default behavior of snakey should be to obtain unmatched requests and return 404 responses.

@ILikePizza555 ILikePizza555 added enhancement New feature or request help wanted Extra attention is needed Important labels Mar 15, 2019
@faithanalog
Copy link

faithanalog commented Mar 16, 2019

I think this can be solved by changing the Snake datatype. I'm going to use haskell for this. Disclaimer: I haven't compiled the code below, so I can't say with 100% certainty that it typechecks, but it should.

So right now you essentially have:

newtype Snake a b = Snake { runSnake :: a -> b }

chain :: Snake a b -> Snake b c -> Snake a c
chain (Snake snakeAB) (Snake snakeBC) = Snake (snakeBC . snakeAB)

This is basically just a wrapper around function composition. I think that what you actually want is Either. Consider:

data Either r a = Left r | Right a

-- I'm omitting the requisite Functor and Applicative instances because I
-- don't feel like writing them.
instance Monad (Either l) where
    (>>=) :: Either r a -> (a -> Either r b) -> Either r b
    Left r >>= f = Left r
    Right a >>= f = f a

-- For convenience, here's an equivalent to Snake & Chain
type Snake r a b = a -> Either r b

chain :: Snake r a b -> Snake r b c -> Snake r a c
chain snakeAB snakeBC =
    \a -> snakeAB a >>= snakeBC

-- Which, when you expand (>>=) looks like
chain snakeAB snakeBC =
    \a -> case snakeAB a of
        Left r -> Left r
        Right b -> snakeBC b

-- And then our identity function
snake :: Snake r a a
snake = \a -> Right a

-- And a way to lift a normal function into a snake
toSnake :: (a -> b) -> Snake r a b
toSnake f = \a -> Right (f a)

Now we can allow a chain to terminate early by returning the left value. We'll just leave it as () for now.

type Route = Snake () Context Response

And let's make a function that lets us combine two snakes, such that:

  • If the left snake returns a response, that response is returned
  • If not, but the right snake returns a response, that response is returned
  • Otherwise, () is returned
-- This is actually mappend from monoid, AKA the <> operator AKA concat
concatRoute :: Route -> Route -> Route
concatRoute leftSnake rightSnake =
    \context ->
        case leftSnake context of
            Left _ -> rightSnake context
            Right response -> Right response

concatAllRoutes :: [Route] -> Route
concatAllRoutes (r:routes) = concatRoute r (concatAllRoutes routes)

Thus, applySnakes can run the concatenated routes, and return a 404 response if none match. The first matching route stops any other routes from running.

-- Let's make a basic 404 response, assuming that
-- withResponseCode :: Int -> Response -> Response
-- since I don't know how the library works
do404 :: Context -> Response
do404 context =
    withResponseCode
        404
        (textResponse
            ("Lol couldn't find " ++ show (uri context)))

applySnakes :: [Route] -> Context -> Response
applySnakes routes =
    \context ->
        case (concatAllRoutes routes) context of
            Left _ -> do404 context
            Right response -> response 

app :: [Route]
app = [
    snake
        `chain` bite "GET" "/"
        `chain` textResponse "Hello World!"
]

sendResponse :: Response -> IO ()
sendResponse = ???

server = \context -> sendResponse (applySnakes app)

This isn't too terrible to translate to typescript, and allows your routes to remain stateless like they are now.

I'm not going to fully implement it, but you could go on from here to add termination reasons for things like access control

type Route = Snake TerminationReason Context Response

data TerminationReason = NoMatch | Unauthorized

-- Implement this with special handling on Unauthorized
applySnakes :: [Route] -> Context -> Response

@ILikePizza555
Copy link
Owner Author

ILikePizza555 commented Mar 16, 2019

I really like this solution of using Either to return the possibility of an error.

There's one issue though: Snake is not a normal function. It's a transformation on an Observable. The current type of Snake is a function (Observable<A>): Observable<B>. Observables emit values asynchronously and operators like Snake allow transformations to be done on the stream as a whole.

Because of this, Either has to be the type B in my previous definition. So the new type of Snake is (Observable<A>): Observable<Either<E, B>>. The issue with this is that now composing Snakes is either impossible or really hard. Previously, I could get by with normal function composition, but now, compose will not type match:

compose(f1: (Observable<A>): Observable<Either<E, B>>, f2: (Observable<B>): Observable<Either<E, C>>)

I thought about redefining Snake to be A -> Observable<Either<E, B>>. This solves the composition problem because I can simply do

(a: Observable<A>) => f1(a).pipe(flatMap((v: Either<E, B>) => v.chain(f2)))

and Snake can still be lifted to a true operator with flatMap.

However, this creates a compatibility issue: RxJS operators are all of the type (Observable<A>): Observable<B>. So it will no longer be possible to provide standard RxJS operators to Snake.chain. Therefore, common operations will need to be re-implemented.

The only other solution I can think of is to abandon Either completely and just have bite throw a certain error type when the match fails, catch all errors in applySnakes on only raise a 404 if every route throws an error. I don't like this solution because of the lack of type-safety.

@ILikePizza555
Copy link
Owner Author

Hmm, it appears that RxJS allows deep access into the Observables for operators. I may just be possible if I construct Snake like this: https://github.com/ReactiveX/rxjs/blob/master/doc/operator-creation.md#creating-an-operator-for-inclusion-in-this-library

ILikePizza555 added a commit that referenced this issue Mar 16, 2019
This is part of fix to #5.

+ Added new method to Snake interface: map
+ Added type alias SnakeFunc
+ Added new method liftSnake
+ Updated tfSnake method
+ Update snake method
+ Added new errorSnake method
- Removed compose method
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request help wanted Extra attention is needed Important
Projects
None yet
Development

No branches or pull requests

2 participants