メモるブログ

技術メモを書いていく所存

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"

なんかデバッグしたい場合はSystem.IO.printで

System.IO.print loginResult

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とかで変換かけれるっぽい

あと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

正規表現 (他にもライブラリあるっぽいしパーサコンビネータであれこれできるっぽいけどregex-posixを利用)

import Text.Regex.Posix

matches = serverUrl =~ ("^(https://[^/]*)/.*" :: String) :: [[String]]