Haskellメモ: Salesforceでクエリするまで
ここのgistに至るまでのメモ
haskell-salesforce.hs · GitHub
データ定義。ログインに必要なデータ型を定義。
LoginResultとしてセッションIDとかもひとまとめにしておく
data LoginRequest = LoginRequest{ username :: String, password :: String, endpoint :: String, apiVersion :: String } deriving Show data LoginResult = LoginResult{ sessionId :: String, userId :: String, serverUrl :: String, metadataServerUrl :: String, instanceUrl :: String, resApiVersion :: String } deriving Show
クエリリザルトとレコードの型定義。
fromJSONはちゃんと型定義が必要なので、もしライブラリ化するとなるとAccountとかのデータ型は随時ライブラリ利用者側で作ってもらう必要がありそう
data QueryResult = QueryResult{ done :: Bool, totalSize :: Int, records :: [Account] } deriving Show instance FromJSON QueryResult where parseJSON (Object v) = QueryResult <$> v .: "done" <*> v .: "totalSize" <*> v .: "records" parseJSON _ = mzero data Account = Account{ sfid :: String, name :: String } deriving Show instance FromJSON Account where parseJSON (Object v) = Account <$> (v .: "Id") <*> (v .: "Name")
上の書き方だと上の属性から順に定義していくっぽい。JSONの属性名とデータ型の属性名は合わせなくてもOK。
上の属性から順に定義するのがダルかったり、色々手を加えたい場合はdoでいろいろやる
{-# LANGUAGE RecordWildCards #-} instance FromJSON QueryResult where parseJSON = withObject "QueryResult" $ \v -> do records <- v .: "records" totalSize <- v .: "totalSize" done <- v .: "done" return QueryResult{..}
RecordWhileCardsは{..}
っていう記法でrecords=records, totalSize=totalSize, ...
みたいにワイルドカード展開してくれる神機能
Generic使うともっと楽でこれだけでOKっぽい
{-# LANGUAGE DeriveGeneric #-} import GHC.Generics instance FromJSON QueryResult where
Aesonの使い方はここが詳しい→ https://artyom.me/aeson
main部分。IO型は特殊な感じ。副作用あれこれ
main :: IO () main = do
コマンドライン引数はgetArgsで
args <- getArgs
環境変数はgetEnvで
username <- getEnv "SALESFORCE_USERNAME"
- http://hackage.haskell.org/package/base-4.12.0.0/docs/System-Environment.html#v:getEnv
- String -> IO String
なんかデバッグしたい場合はSystem.IO.printで
System.IO.print loginResult
- http://hackage.haskell.org/package/base-4.12.0.0/docs/Prelude.html#v:print
- Show a => a -> IO ()
IOを返すけど戻り値を無視すると print XXX >> fでprint XXX >>= _ -> と等価だった(気がする)
なので連続して実行するのと変わらないって感じ
文字列replaceはそんな関数用意されいないっぽいので自前で
replace :: String -> String -> String -> String replace x y src = inner src where inner [] = [] inner str@(s:ss) | L.isPrefixOf x str = y ++ inner (L.drop (L.length x) str) | otherwise = s:inner ss
isPrefixOfで合致したらその文字列を削る L.drop (L.length)
してその文字列をくっつけて y ++
それを再帰的に実行すればOK
HTTPリクエスト、正直よくわからないけどこんな感じでできた
import Network.HTTP.Conduit import Network.URI import Network.URI.Encode as URI initReq <- parseRequest $ "https://" ++ endpoint ++ "/services/Soap/u/" ++ apiVersion manager <- newManager tlsManagerSettings response <- httpLbs req manager
XMLパースはもっとよくわからない。
let Document _ _ root _ = xmlParse "" $ (BL8.unpack (responseBody response)) cont = CElem root noPos result = xtract id "/soapenv:Envelope/soapenv:Body/loginResponse/result" cont !! 0 sessionId = tagTextContent $ xtract id "/result/sessionId" result !! 0
Data.ByteString.Char8
とか Data.ByteString.Lazy.Char8
とか Data.Text
とかあんまよくわかってない…
とりあえずpackとかunpackとかで変換かけれるっぽい
- http://hackage.haskell.org/package/bytestring-0.10.8.2/docs/Data-ByteString-Char8.html#v:unpack
- http://hackage.haskell.org/package/bytestring-0.10.8.2/docs/Data-ByteString.html#v:unpack
- http://hackage.haskell.org/package/text-1.2.3.1/docs/Data-Text-Lazy.html#v:unpack
あとOverloadedStringsを使うと文字列リテラルでTextやByteStringを定義できるようになって便利。IsStringインスタンスが定義されていれば何でもいける…?
{-# LANGUAGE OverloadedStrings #-}
NamedFieldPuns使うとPerson{name=name} みたいな冗長な記述を排除できて便利
{-# LANGUAGE NamedFieldPuns #-} return LoginResult{sessionId, userId, ...}
URIエンコードするときはNetwork.URI.Encode.encodeで
let queryUrl = instanceUrl lr ++ "/services/data/v" ++ resApiVersion lr ++ "/query/?q=" ++ URI.encode query
Data.Aeson.decodeはMaybe型を返す
let result = JSON.decode body :: Maybe QueryResult
- http://hackage.haskell.org/package/aeson-1.4.2.0/docs/Data-Aeson.html#v:decode
- FromJSON a => ByteString -> Maybe a
正規表現 (他にもライブラリあるっぽいしパーサコンビネータであれこれできるっぽいけどregex-posixを利用)
import Text.Regex.Posix matches = serverUrl =~ ("^(https://[^/]*)/.*" :: String) :: [[String]]