packages feed

servant-ruby (empty) → 0.1.0.0

raw patch · 6 files changed

+449/−0 lines, 6 filesdep +Cabaldep +basedep +casingsetup-changed

Dependencies added: Cabal, base, casing, doctest, lens, servant-foreign, text

Files

+ LICENSE view
@@ -0,0 +1,30 @@+Copyright Hardy Jones (c) 2017++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++    * Redistributions of source code must retain the above copyright+      notice, this list of conditions and the following disclaimer.++    * Redistributions in binary form must reproduce the above+      copyright notice, this list of conditions and the following+      disclaimer in the documentation and/or other materials provided+      with the distribution.++    * Neither the name of Author name here nor the names of other+      contributors may be used to endorse or promote products derived+      from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,3 @@+# servant-ruby++Create a Ruby client from a Servant API using `Net::HTTP`.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ servant-ruby.cabal view
@@ -0,0 +1,37 @@+name:                servant-ruby+version:             0.1.0.0+synopsis:            Generate a Ruby client from a Servant API with Net::HTTP.+description:         Generate a Ruby client from a Servant API with Net::HTTP.+homepage:            https://github.com/joneshf/servant-ruby#readme+license:             BSD3+license-file:        LICENSE+author:              Hardy Jones+maintainer:          jones3.hardy@gmail.com+copyright:           2017 Hardy Jones+category:            Servant, Web+build-type:          Simple+extra-source-files:  README.md+cabal-version:       >=1.10++library+  hs-source-dirs:      src+  exposed-modules:     Servant.Ruby+  build-depends:       base >= 4.7 && < 5+                     , casing >= 0.1 && < 0.2+                     , lens >= 4.15 && < 4.16+                     , servant-foreign >= 0.9 && < 0.10+                     , text >= 1.2 && < 1.3+  default-language:    Haskell2010+  ghc-options:         -Wall++test-suite doc-test+  type:                exitcode-stdio-1.0+  main-is:             Main.hs+  hs-source-dirs:      test+  build-depends:       base+                     , Cabal >= 1.24 && < 1.25+                     , doctest >= 0.11 && < 0.12++source-repository head+  type:     git+  location: https://github.com/joneshf/servant-ruby
+ src/Servant/Ruby.hs view
@@ -0,0 +1,370 @@+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}++{-|+Module: Servant.Ruby+Description: Generate a Ruby client from a Servant API using Net::HTTP.+Copyright: (c) Hardy Jones, 2017+License: BSD3+Maintainer: jones3.hardy@gmail.com+Stability: Experimental++-}++module Servant.Ruby (NameSpace(..), ruby) where++import Control.Lens (filtered, folded, to, view, (^.), (^..))++import Data.Function ((&))+import Data.Monoid ((<>))+import Data.Proxy (Proxy(Proxy))+import Data.Text (Text)+import Data.Text.Encoding (decodeUtf8')++import Servant.Foreign+  ( ArgType(Flag, List, Normal)+  , Foreign+  , GenerateList+  , HasForeign+  , HeaderArg+  , NoContent+  , NoTypes+  , QueryArg+  , Req+  , Segment(Segment)+  , SegmentType(Cap, Static)+  , argName+  , argPath+  , captureArg+  , headerArg+  , isCapture+  , listFromAPI+  , path+  , queryArgName+  , queryArgType+  , queryStr+  , reqBody+  , reqFuncName+  , reqHeaders+  , reqMethod+  , reqUrl+  , snakeCaseL+  , _PathSegment+  )++import Text.Casing (quietSnake)++import qualified Data.Text as T++-- $setup+-- >>> :set -XDataKinds+-- >>> :set -XOverloadedStrings+-- >>> :set -XTypeOperators+-- >>> :m + Servant.API+-- >>> :m + Data.Text.IO++{-|+The namespace for the generated class.+-}+data NameSpace+  = NameSpace+    { moduleNames :: [Text]+      -- ^ The list of namespaces you'd like the class to appear in.+    , className   :: Text+      -- ^ The name of the class you'd like the API methods to appear in.+    }++{-|+Generate a Ruby class with methods for the Servant API.++Currently assumes the API accepts and returns JSON.++For example:++>>> Data.Text.IO.putStr $ ruby (NameSpace [] "Baz") (Proxy :: Proxy (Get '[JSON] ()))+require "json"+require "net/http"+require "uri"+<BLANKLINE>+class Baz+  def initialize(origin)+    @origin = URI(origin)+    @http = Net::HTTP.new(@origin.host, @origin.port)+  end+<BLANKLINE>+  def get()+    uri = URI("#{origin}")+<BLANKLINE>+    req = Net::HTTP::Get.new(uri)+    req["Accept"] = "application/json"+<BLANKLINE>+    request(req)+  end+<BLANKLINE>+  private+<BLANKLINE>+  def request(req, body = nil)+    res = http.request(req, body)+    res.body = JSON.parse(res.body, symbolize_names: true)+    res+  end+end++The class can be nested in a module namespace if you choose so.++>>> Data.Text.IO.putStr $ ruby (NameSpace ["Foo", "Bar"] "Baz") (Proxy :: Proxy (Get '[JSON] ()))+require "json"+require "net/http"+require "uri"+<BLANKLINE>+module Foo+  module Bar+    class Baz+      def initialize(origin)+        @origin = URI(origin)+        @http = Net::HTTP.new(@origin.host, @origin.port)+      end+<BLANKLINE>+      def get()+        uri = URI("#{origin}")+<BLANKLINE>+        req = Net::HTTP::Get.new(uri)+        req["Accept"] = "application/json"+<BLANKLINE>+        request(req)+      end+<BLANKLINE>+      private+<BLANKLINE>+      def request(req, body = nil)+        res = http.request(req, body)+        res.body = JSON.parse(res.body, symbolize_names: true)+        res+      end+    end+  end+end++Captures and query parameters are translated into required arguments, in that order.++The request body and headers are translated into keyword arguments, in that order.++>>> let api = Proxy :: Proxy ("foo" :> Capture "fooId" Int :> ReqBody '[JSON] () :> QueryParam "bar" Bool :> Header "Max-Forwards" Int :> Post '[JSON] ())+>>> Data.Text.IO.putStr $ ruby (NameSpace [] "Foo") api+require "json"+require "net/http"+require "uri"+<BLANKLINE>+class Foo+  def initialize(origin)+    @origin = URI(origin)+    @http = Net::HTTP.new(@origin.host, @origin.port)+  end+<BLANKLINE>+  def post_foo_by_foo_id(foo_id, bar, body:, max_forwards:)+    uri = URI("#{origin}/foo/#{fooId}?bar=#{bar}")+<BLANKLINE>+    req = Net::HTTP::Post.new(uri)+    req["Accept"] = "application/json"+    req["Content-Type"] = "application/json"+    req["Max-Forwards"] = max_forwards+<BLANKLINE>+    request(req, body)+  end+<BLANKLINE>+  private+<BLANKLINE>+  def request(req, body = nil)+    res = http.request(req, body)+    res.body = JSON.parse(res.body, symbolize_names: true)+    res+  end+end+-}+ruby+  :: (GenerateList NoContent (Foreign NoContent api), HasForeign NoTypes NoContent api)+  => NameSpace+  -> Proxy api+  -> Text+ruby (NameSpace {..}) p =+  T.unlines $+    imports+    ++ prologue+    ++ body+    ++ epilogue+  where+  body =+    initialize indent+    ++ foldMap (public indent) api+    ++ private indent+  api =+    listFromAPI (Proxy :: Proxy NoTypes) (Proxy :: Proxy NoContent) p+  prologue =+    ((\(i, n) -> T.replicate i "  " <> "module " <> n) <$> zip [0..] moduleNames)+    ++ [T.replicate indent "  " <> "class " <> className]+  epilogue =+    reverse $ (\i -> T.replicate i "  " <> "end") <$> [0..indent]+  indent =+    length moduleNames++imports :: [Text]+imports =+  [ "require \"json\""+  , "require \"net/http\""+  , "require \"uri\""+  , ""+  ]++properIndent :: Functor f => Int -> f (Maybe Text) -> f Text+properIndent indent =+  fmap (maybe "" (T.replicate (indent + 1) "  " <>))++initialize :: Int -> [Text]+initialize indent =+  properIndent indent+    [ Just "def initialize(origin)"+    , Just "  @origin = URI(origin)"+    , Just "  @http = Net::HTTP.new(@origin.host, @origin.port)"+    , Just "end"+    ]++private :: Int -> [Text]+private indent =+  properIndent indent+    [ Nothing+    , Just "private"+    , Nothing+    , Just "def request(req, body = nil)"+    , Just "  res = http.request(req, body)"+    , Just "  res.body = JSON.parse(res.body, symbolize_names: true)"+    , Just "  res"+    , Just "end"+    ]++public :: Int -> Req NoContent -> [Text]+public indent req =+  properIndent indent $+    [ Nothing+    , Just $ "def " <> functionName <> "(" <> argsStr <> ")"+    , Just $ "  uri = URI(" <> url <> ")"+    , Nothing+    , Just $ "  req = Net::HTTP::" <> method <> ".new(uri)"+    ]+    ++ requestHeaders+    +++    [ Nothing+    , Just $ "  request" <> request+    , Just "end"+    ]+  where+  functionName :: Text+  functionName = req ^. reqFuncName.snakeCaseL.to snake++  argsStr  :: Text+  argsStr = T.intercalate ", " $ snake <$> args++  args :: [Text]+  args =+    captures+    ++ ((^. queryArgName.argPath) <$> queryparams)+    ++ body+    ++ headerArgs++  segments :: [Segment NoContent]+  segments = req ^. reqUrl.path^..folded.filtered isCapture++  queryparams :: [QueryArg NoContent]+  queryparams = req ^.. reqUrl.queryStr.traverse++  captures :: [Text]+  captures = view argPath . captureArg <$> segments++  body :: [Text]+  body =+    case req ^. reqBody of+      Just _  -> ["body:"]+      Nothing -> []++  request :: Text+  request =+    case req ^. reqBody of+      Just _  -> "(req, body)"+      Nothing -> "(req)"++  accept :: [Maybe Text]+  accept = [Just "  req[\"Accept\"] = \"application/json\""]++  contentType :: [Maybe Text]+  contentType =+    case req ^. reqBody of+      Just _  -> [Just "  req[\"Content-Type\"] = \"application/json\""]+      Nothing -> []++  hs :: [HeaderArg NoContent]+  hs = req ^. reqHeaders++  requestHeaders :: [Maybe Text]+  requestHeaders =+    accept+    ++ contentType+    ++ (requestHeader <$> rawHeaders)++  headerArgs :: [Text]+  headerArgs = (<> ":") <$> rawHeaders++  rawHeaders :: [Text]+  rawHeaders = (^. headerArg.argName._PathSegment) <$> hs++  method :: Text+  method =+    case req ^. reqMethod.to decodeUtf8' of+      Right m -> T.toTitle m+      Left _  -> "Get"++  url :: Text+  url = "\"#{origin}" <> urlArgs <> queryArgs <> "\""++  urlArgs :: Text+  urlArgs = req ^.. reqUrl.path.traverse & rbSegments++  queryArgs :: Text+  queryArgs =+    if null queryparams then+      ""+    else+      "?" <> rbParams "&" queryparams++requestHeader :: Text -> Maybe Text+requestHeader header = Just $ "  req[\"" <> header <> "\"] = " <> snake header++rbSegments :: [Segment f] -> Text+rbSegments []     = ""+rbSegments [x]    = "/" <> segmentToStr x+rbSegments (x:xs) = "/" <> segmentToStr x <> rbSegments xs++segmentToStr :: Segment f -> Text+segmentToStr (Segment st) = segmentTypeToStr st++segmentTypeToStr :: SegmentType f -> Text+segmentTypeToStr (Static s) = s ^. _PathSegment+segmentTypeToStr (Cap s)    = "#{" <> s ^. argName._PathSegment <> "}"++rbParams :: Text -> [QueryArg f] -> Text+rbParams _ []     = ""+rbParams _ [x]    = paramToStr x+rbParams s (x:xs) = paramToStr x <> s <> rbParams s xs++paramToStr :: QueryArg f -> Text+paramToStr qarg =+  case qarg ^. queryArgType of+    Normal -> key <> "=#{" <> val <> "}"+    Flag   -> key+    List   -> key <> "[]=#{" <> val <> "}"+  where+  key = qarg ^. queryArgName.argName._PathSegment+  val = snake key++snake :: Text -> Text+snake = T.pack . quietSnake . T.unpack
+ test/Main.hs view
@@ -0,0 +1,7 @@+module Main where++import Test.DocTest (doctest)++main :: IO ()+main =+  doctest ["-isrc", "src"]