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

File extensions #283

Closed
wants to merge 7 commits into from
Closed

Conversation

axman6
Copy link
Contributor

@axman6 axman6 commented Dec 3, 2015

Adds the ability to capture Text file names with an extension specified in the type.

           -- GET /file/:filename.png
type MyApi = "file" :> Capture "filename" (Ext "json") :> Get '[JSON] Text

-- when accessing "/file/foo.json", prints:
--     foo.json
--     foo
handler :: Ext "json" -> ExceptT ServantErr IO Text
handler t@(Ext n) = return (renderExt t <> "\n" <> n)

We serve several server generated CSV and PNG files which currently have URLs which do not contain the file extension, which confuses some browsers, despite the correct content type.

I tried using something more generic than Text for the file name, but it proved to be not very useful - supporting a generic From/ToHttpApiData type only makes sense sometimes.

@freezeboy
Copy link
Contributor

Why not a composition using (:>) operator ? this would allow the use of this extension with other stuff than Capture ? (I have no example in mind right now)

@axman6
Copy link
Contributor Author

axman6 commented Dec 3, 2015

This can also be used in query parameters and headers through the use of the To/FromHttpApiData, not just Captures. Maybe I've misunderstood your point @freezeboy.

@axman6
Copy link
Contributor Author

axman6 commented Dec 3, 2015

Also :> is used to split the URL path by /'s not full stops, so having something like: Capture "file name" Text :> ".png" ... matches things like foo/.png.

@dmjio
Copy link
Member

dmjio commented Dec 3, 2015

@axman6 why not just make your own combinator for this?

data Ex (fileName :: Symbol) (extension :: Symbol)
instance (KnownSymbol fileName, KnownSymbol extension, HasServer api) => 
     HasServer (Ex fileName ext :> api) where
 .... 

@axman6
Copy link
Contributor Author

axman6 commented Dec 3, 2015

The file name is the part were trying to capture, so it can't be a static part of the type. The helper allows you to handle requests for any file with a given extension instead of having to leave the extension off the URL.

@axman6
Copy link
Contributor Author

axman6 commented Dec 3, 2015

This implementation gets a lot of benefits for free, it can be used in several places in the URL and request without having to implement HasServer or anything else.

@dmjio
Copy link
Member

dmjio commented Dec 3, 2015

@axman6 Ah I see, of course the filename has to be dynamic =) I guess then you could in theory do something like this:

data Ex (extension :: Symbol)
instance (KnownSymbol extension, HasServer api) => HasServer (Ex fileName ext :> api) where
 ... capture filename in path, ensure it matches extension, if no extension, then still try stuff ....

@axman6
Copy link
Contributor Author

axman6 commented Dec 3, 2015

Sure, but this already does that when used with Capture, ensures the extension is present and strips it (but it's recoverable because the type is known)

@freezeboy
Copy link
Contributor

@axman6 I only read your description quckly, now I reviewed your commit and I understand better what you call an extension. I though you added a 'parameter' to Capture only, I like your implementation. sorry for the noise

@jkarni
Copy link
Member

jkarni commented Jan 6, 2016

I see the point. It's a little annoying that content-types are not sufficient in all cases, but I guess we have to come to terms with that. @axman6 do you have a reference for when browsers misbehave w.r.t. content-types (but not extensions)?

(Also, you'll have to add a CPP'd import of mappend to satisfy the 7.8 build.)

import Data.Maybe (catMaybes)
import Control.Monad ((>=>))

-- | A wrapper around a time type which can be parsed/rendered to with `format',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Time? This seems like it's from the other PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whoops, yes, they we're developed at the tame time and I guess the two branches weren't kept as sterile as they should have been =)

@jkarni
Copy link
Member

jkarni commented Jan 8, 2016

Thanks for the fixes.

General questions I should have asked earlier:

  • The more general combinator is, I would think, a Regex one (which however we probably wouldn't want in the main repo since it would add annoying dependencies). Is there a reason to prefer this over a Regex combinator?
  • There is no connection between the content-type and the extension. In particular, we can't accept multiple extensions that correspond to content-types that can be served. This seems like it would be confusing, but a clearer sense of when browsers ignore content-types (but not extensions) would be helpful.
  • When should this be used? (Related to the above.)

@phadej
Copy link
Contributor

phadej commented Jan 13, 2017

From what I can see, this functionality doesn't need to leave in servant.

cc @alpmestan, I guess this one could be used as an example of writing your own combinators.

FWIW, it might make sense to setup servant-contrib package (not to be released, if we don't want) to accumulate this kind of useful, but not enough combinators / stuff. Also to act as wider "what I will break with this change" -bench.

@alpmestan
Copy link
Contributor

Yes, servant-contrib/extras sounds like something we have to put somewhere on the roadmap now. Maybe servant-multipart could be merged into it? I don't know really. I'm not fond of -contrib/-extras packages but don't see a better solution here. It's however tough to decide what should go in there and what should be released in an individual package.

@phadej
Copy link
Contributor

phadej commented Jan 13, 2017

Yes, it's hard to draw a line. I'd keep multipart as a separate one. But Ext is too small to justify a package. OTOH, if we go with @jkarni idea of regexp captures than, servant-regex-capture might make sense as a single package. Tough choices.

-- | A type for quick'n'dirty capture / query param matching.
--
-- @n@ stands for capture groups to extract
--
-- ==== Example
--
-- @
-- type API = "folder" :> Capture "filename" (RE "(.*)\\.json") :> Get '[JSON] MyType
-- @
newtype RE n = RE { matchGroups :: Vec n Text }

EDIT: if it's in a separate package, than it can depend on e.g. swagger2 do define proper schemas, not the way around (which would be difficult).

@phadej phadej mentioned this pull request Jan 20, 2017
@basvandijk
Copy link

basvandijk commented Jan 31, 2017

I need something like this however for my use case I would prefer that Ext is polymorphic in the filename as in:

newtype Ext (ext :: Symbol) a = Ext {getBase :: a}
instance (KnownSymbol ext, ToHttpApiData a) => ToHttpApiData (Ext ext a) where ...

So that I can define the following for example:

type MyApi = "file" :> Capture "filename" (Ext "jpg" UTCTime) :> Raw

Does that make sense?

EDIT:
If we change Ext to a binary operator we might consider turning it into an infix operator like:

newtype (:.) a (ext :: Symbol) = Ext (getBase :: a)

so that I can define my example as:

type MyApi = "file" :> Capture "filename" (UTCTime :. "jpg") :> Raw

@phadej
Copy link
Contributor

phadej commented Jan 31, 2017

@basvandijk have you seen: https://github.com/haskell-servant/servant-contrib/blob/master/servant-contrib/src/Servant/Contrib/RegexHttpApiData.hs

Consensus here is, that we are unlikely to add this combinator to servant as it's a) quite easy to make/copy&paste yourself b) many choices to do.

@basvandijk
Copy link

@phadej thanks for the heads up. I'll copy&paste&adapt the code then.

@phadej
Copy link
Contributor

phadej commented Feb 1, 2017

FWIW: it's not even servant related, as defining your Ext or RE needs only http-api-data.

@phadej
Copy link
Contributor

phadej commented May 14, 2017

Closing; the regex code is in servant-contrib

@phadej phadej closed this May 14, 2017
@domenkozar
Copy link
Contributor

Just as a reference for anyone else ending up here: another option is to use newtype businiess-like wrappers and define your own url parsing as per http://haskell-servant.readthedocs.io/en/stable/tutorial/Server.html#the-fromhttpapidata-tohttpapidata-classes

adpextwindong added a commit to adpextwindong/RetroMMO-api-hclient that referenced this pull request Nov 19, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

8 participants