erpnext-api-client 0.1.0.0 → 0.1.0.1
raw patch · 7 files changed
+75/−29 lines, 7 filessetup-changedPVP ok
version bump matches the API change (PVP)
API changes (from Hackage documentation)
Files
- README.md +20/−8
- Setup.hs +0/−2
- erpnext-api-client.cabal +1/−1
- src/ERPNext/Client.hs +38/−8
- src/ERPNext/Client/Filters.hs +1/−0
- src/ERPNext/Client/Helper.hs +4/−0
- src/ERPNext/Client/QueryStringParams.hs +11/−10
README.md view
@@ -61,7 +61,7 @@ manager <- newManager defaultManagerSettings response <- getDocType manager config "Company A" :: IO (ApiResponse Customer) case response of- Ok _ c _ -> putStrLn $ name c+ Ok _ _ c -> putStrLn $ name c Err r _ -> print r ``` @@ -103,9 +103,23 @@ dependency. The resulting record types can be used together with this API client but the `IsDocType` instance must still be defined by hand. -****The API client part generated from the OpenAPI spec can not be-used.****+**The API client part generated from the OpenAPI spec can not be used.** +Example models file:++``` plain+SalesOrder+ name Text+ total Double+ transaction_date Text+ items [SalesOrderItem]+ Required name++SalesOrderItem+ name Text+ Required name+```+ ``` example $ ./scripts/gen-openapi-yaml.sh models > openapi.yaml $ openapi3-code-generator-exe \@@ -125,13 +139,11 @@ │ │ ├── SecuritySchemes.hs │ │ ├── TypeAlias.hs │ │ ├── Types <---- here are the generated types-│ │ │ ├── Customer.hs-│ │ │ ├── Order.hs-│ │ │ ├── Supplier.hs+│ │ │ ├── SalesOrder.hs+│ │ │ ├── SalesOrderItem.hs │ │ └── Types.hs │ └── ERPNextAPI.hs-├── stack.yaml-└── stack.yaml.lock+└── stack.yaml ``` ## Note on TLS problems
− Setup.hs
@@ -1,2 +0,0 @@-import Distribution.Simple-main = defaultMain
erpnext-api-client.cabal view
@@ -1,7 +1,7 @@ cabal-version: 3.6 name: erpnext-api-client-version: 0.1.0.0+version: 0.1.0.1 synopsis: Generic API client library for ERPNext description: This is a Haskell API client for ERPNext. It aims to be a light-weight library based on http-client and user-provided record types. homepage: https://github.com/schoettl/erpnext-api-client
src/ERPNext/Client.hs view
@@ -2,6 +2,17 @@ {-# LANGUAGE TypeApplications #-} {-# LANGUAGE OverloadedStrings #-} +{-|+Description: Generic API client library for ERPNext++This is a Haskell API client for ERPNext. It aims to be a light-weight+library based on http-client and user-provided record types.++API documentation:++https://docs.frappe.io/framework/user/en/api/rest+-}+ module ERPNext.Client ( getDocTypeList , getDocType@@ -29,11 +40,16 @@ import ERPNext.Client.Helper (urlEncode) -- | Type class for types which represent an ERPNext DocType.--- Each DocType has a unique name.+-- Each DocType has a unique name but there can still be multiple+-- „views“ (i.e. records types) for one DocType. class IsDocType a where docTypeName :: Text- -- TODO: implement auto-derive (using typename and generic)? +{-|+ Get a list of all documents of a given DocType.+ The 'QueryStringParam's can select fields, filter, order, enable+ paging, and more.+-} getDocTypeList :: forall a. (IsDocType a, FromJSON a) => Manager -> Config -> [QueryStringParam]-> IO (ApiResponse [a]) getDocTypeList manager config qsParams = do@@ -42,6 +58,7 @@ response <- httpLbs request manager return $ parseGetResponse response +-- | Get a single document of a given DocType by name. getDocType :: forall a. (IsDocType a, FromJSON a) => Manager -> Config -> Text -> IO (ApiResponse a) getDocType manager config name = do@@ -50,13 +67,13 @@ response <- httpLbs request manager return $ parseGetResponse response -{- | Delete a named object.+{- | Delete a single document of a given DocType by name. The phantom type parameter @a@ is used to figure out the DocType. A customer can be deleted like this: @-res <- deleteDocType @Customer manager config "<customer name>"+res \<- deleteDocType @Customer manager config "customer name" @ -} deleteDocType :: forall a. (IsDocType a)@@ -67,6 +84,7 @@ response <- httpLbs request manager return $ parseDeleteResponse response +-- | Create a new document of a given DocType. postDocType :: forall a. (IsDocType a, FromJSON a, ToJSON a) => Manager -> Config -> a -> IO (ApiResponse a) postDocType manager config doc = do@@ -75,6 +93,7 @@ response <- httpLbs request manager return $ parseGetResponse response +-- | Update a document of a given DocType by name. putDocType :: forall a. (IsDocType a, FromJSON a, ToJSON a) => Manager -> Config -> Text -> a -> IO (ApiResponse a) putDocType manager config name doc = do@@ -83,7 +102,7 @@ response <- httpLbs request manager return $ parseGetResponse response -+-- | Create an API client configuration. mkConfig :: Text -> Text -> Secret -> Config mkConfig baseUrl apiKey apiSecret = Config { baseUrl = baseUrl@@ -96,7 +115,7 @@ mkSecret = Secret --- | Create the API Request.+-- | Create the API 'Request'. createRequest :: Config -> Text -> BS.ByteString -> IO Request createRequest config path method = do request <- parseRequest $ unpack (baseUrl config <> path)@@ -105,6 +124,7 @@ , requestHeaders = [mkAuthHeader config] } +-- | Create the API 'Request' with a JSON body. createRequestWithBody :: ToJSON a => Config -> Text -> BS.ByteString -> a -> IO Request createRequestWithBody config path method doc = do request <- parseRequest $ unpack (baseUrl config <> path)@@ -126,6 +146,7 @@ { getSecret :: Text } +-- | Data wrapper type just to parse the JSON returned by ERPNext. data DataWrapper a = DataWrapper { getData :: a } deriving Show @@ -134,11 +155,20 @@ dataValue <- obj .: "data" return (DataWrapper dataValue) +-- | The API response. data ApiResponse a- = Ok (Response LBS.ByteString) Value a- | Err (Response LBS.ByteString) (Maybe (Value, Text))+ = Ok -- ^ The OK response.+ (Response LBS.ByteString) -- ^ The server's full response including header information.+ Value -- ^ The returned JSON.+ a -- ^ The result parsed from the returned JSON.+ | Err -- ^ The error response.+ (Response LBS.ByteString) -- ^ The server's full response including header information.+ (Maybe (Value, Text)) -- ^ If the response is valid JSON, 'Just' the returned JSON and+ -- the parse error message telling why 'Value' couldn't be parsed+ -- into @a@. deriving Show +-- | Get the full response from the API response. getResponse :: ApiResponse a -> Response LBS.ByteString getResponse (Ok r _ _) = r getResponse (Err r _) = r
src/ERPNext/Client/Filters.hs view
@@ -81,6 +81,7 @@ <> renderFilterValue (filterValue f) <> "]" +-- | Render the filter terms for the URL query string. renderFilters :: Text -> [Filter] -> Text renderFilters prefix filters = let
src/ERPNext/Client/Helper.hs view
@@ -10,15 +10,19 @@ import Data.Text qualified as T import Network.URI (escapeURIString, isUnreserved) +-- | Percent-encode string for use in a URL. urlEncode :: Text -> Text urlEncode = pack . escapeURIString isUnreserved . unpack sanitizeQuotes :: Text -> Text sanitizeQuotes = T.filter (/= '"') +-- TODO: is it a good idea to just drop "?+-- | Double-quote string after dropping existing double quotes. quote :: Text -> Text quote t = "\"" <> sanitizeQuotes t <> "\"" +-- | 'show' but return 'Text'. tshow :: Show a => a -> Text tshow = pack . show
src/ERPNext/Client/QueryStringParams.hs view
@@ -14,22 +14,22 @@ -- TODO: Maybe change type or make opaque type to prevent invalid combinations? -- TODO: add variants for limit, offset, etc.? data QueryStringParam- = Debug Bool -- ^ if 'True', makes API returning query analysis info instead of data- | AsDict Bool -- ^ if 'False', makes API returning the data records as mixed-type arrays which cannot be parsed by this library (default: 'True')- | LimitStart Int -- ^ offset- | LimitPageLength Int -- ^ page size- | Asc Text- | Desc Text- | Fields [Text]- | AndFilter [Filter]- | OrFilter [Filter]+ = Debug Bool -- ^ If 'True', makes API returning query analysis info instead of data+ | AsDict Bool -- ^ If 'False', makes API returning the data records as mixed-type arrays which cannot be parsed by this library (default: 'True')+ | LimitStart Int -- ^ Page offset (starts at 0)+ | LimitPageLength Int -- ^ Page size+ | Asc Text -- ^ Ascending order by given field+ | Desc Text -- ^ Descending order by given field+ | Fields [Text] -- ^ Select fields+ | AndFilter [Filter] -- ^ Filter terms combined with logical AND+ | OrFilter [Filter] -- ^ Filter terms combined with logical OR renderQueryStringParam :: QueryStringParam -> Text renderQueryStringParam qsParam = case qsParam of Debug b -> "debug=" <> tshow b AsDict b -> "as_dict=" <> tshow b- LimitStart offset -> "limit_start=" <> tshow offset+ LimitStart offset -> "limit_start=" <> tshow (max 0 offset) LimitPageLength n -> "limit=" <> tshow n -- it was limit_page_length up to v13 Asc field ->@@ -55,5 +55,6 @@ renderOrderBy field order = "order_by=" <> urlEncode (field <> " " <> order) +-- | Render the query string for the URL. renderQueryStringParams :: [QueryStringParam] -> Text renderQueryStringParams params = intercalate "&" (map renderQueryStringParam params)