VKHS 0.3.3 → 0.5.0
raw patch · 15 files changed
+1114/−136 lines, 15 filesdep −fclabelsdep ~aesondep ~basedep ~bytestringnew-component:exe:vknews
Dependencies removed: fclabels
Dependency ranges changed: aeson, base, bytestring, containers, mtl, optparse-applicative, template-haskell
Files
- VKHS.cabal +33/−12
- src/Data/Label.hs +166/−0
- src/Data/Label/Abstract.hs +133/−0
- src/Data/Label/Derive.hs +218/−0
- src/Data/Label/Maybe.hs +75/−0
- src/Data/Label/MaybeM.hs +31/−0
- src/Data/Label/Pure.hs +46/−0
- src/Data/Label/PureM.hs +72/−0
- src/Test/API.hs +1/−1
- src/VKNews.hs +133/−0
- src/VKQ.hs +67/−58
- src/Web/VKHS/API.hs +3/−41
- src/Web/VKHS/API/Aeson.hs +49/−22
- src/Web/VKHS/API/Monad.hs +76/−0
- src/Web/VKHS/API/Types.hs +11/−2
VKHS.cabal view
@@ -1,6 +1,6 @@ name: VKHS-version: 0.3.3+version: 0.5.0 synopsis: Provides access to Vkontakte social network via public API description: Provides access to Vkontakte API methods. Library requires no interaction@@ -20,6 +20,20 @@ type: git location: https://github.com/ierton/vkhs.git +executable vknews+ extra-libraries: curl+ hs-source-dirs: src+ main-is: VKNews.hs+ build-tools: hsc2hs+ ghc-options: -Wall -fwarn-tabs+ other-modules:+ Network.Curlhs.Base,+ Network.Curlhs.Core,+ Network.Curlhs.Easy,+ Network.Curlhs.Setopt,+ Network.Curlhs.Errors,+ Network.Curlhs.Types+ executable vkq extra-libraries: curl hs-source-dirs: src@@ -36,7 +50,7 @@ library- extra-libraries: curl+ --extra-libraries: curl build-tools: hsc2hs ghc-options: -Wall -fwarn-tabs hs-source-dirs: src@@ -55,7 +69,14 @@ Network.Curlhs.Easy, Network.Curlhs.Setopt, Network.Curlhs.Errors,- Network.Curlhs.Types+ Network.Curlhs.Types,+ Data.Label.Abstract,+ Data.Label.Derive,+ Data.Label.Maybe,+ Data.Label.MaybeM,+ Data.Label.Pure,+ Data.Label.PureM,+ Data.Label -- maybe not elegant, but convenient during tests -- if os(windows)@@ -66,27 +87,27 @@ Web.VKHS.API Web.VKHS.API.Aeson Web.VKHS.API.Types+ Web.VKHS.API.Monad Web.VKHS.Curl Web.VKHS.Login Web.VKHS.Types - build-depends: base >=4.5 && <5,- containers ==0.5.*,+ build-depends: base >=4.6 && <5,+ containers,+ mtl,+ bytestring, tagsoup-parsec ==0.0.*, tagsoup ==0.12.*,- mtl ==2.1.*, failure ==0.2.*,- bytestring >=0.10 && <0.11, safe ==0.3.*, parsec ==3.1.*, split ==0.2.*, utf8-string ==0.3.*, bimap ==0.2.*,- template-haskell ==2.8.*,+ template-haskell, transformers ==0.3.*,- fclabels >=1.1.4 && <1.1.6,- optparse-applicative >=0.4 && <0.6,- aeson >= 0.6 && < 0.7,+ optparse-applicative,+ aeson, filepath, directory, regexpr,@@ -94,4 +115,4 @@ vector, text, time- +
+ src/Data/Label.hs view
@@ -0,0 +1,166 @@+{-# LANGUAGE TypeOperators #-}+{- |+This package provides first class labels that can act as bidirectional record+fields. The labels can be derived automatically using Template Haskell which+means you don't have to write any boilerplate yourself. The labels are+implemented as lenses and are fully composable. Labels can be used to /get/,+/set/ and /modify/ parts of a datatype in a consistent way.+-}++module Data.Label+(++-- * Working with @fclabels@.++{- |+The lens datatype, conveniently called `:->', is an instance of the+"Control.Category" type class: meaning it has a proper identity and+composition. The library has support for automatically deriving labels from+record selectors that start with an underscore.++To illustrate this package, let's take the following two example datatypes.+-}++-- |+-- >{-# LANGUAGE TemplateHaskell, TypeOperators #-}+-- >import Control.Category+-- >import Data.Label+-- >import Prelude hiding ((.), id)+-- >+-- >data Person = Person+-- > { _name :: String+-- > , _age :: Int+-- > , _isMale :: Bool+-- > , _place :: Place+-- > } deriving Show+-- >+-- >data Place = Place+-- > { _city+-- > , _country+-- > , _continent :: String+-- > } deriving Show++{- |+Both datatypes are record types with all the labels prefixed with an+underscore. This underscore is an indication for our Template Haskell code to+derive lenses for these fields. Deriving lenses can be done with this simple+one-liner:++>mkLabels [''Person, ''Place]++For all labels a lens will created.++Now let's look at this example. This 71 year old fellow, my neighbour called+Jan, didn't mind using him as an example:++>jan :: Person+>jan = Person "Jan" 71 True (Place "Utrecht" "The Netherlands" "Europe")++When we want to be sure Jan is really as old as he claims we can use the `get`+function to get the age out as an integer:++>hisAge :: Int+>hisAge = get age jan++Consider he now wants to move to Amsterdam: what better place to spend your old+days. Using composition we can change the city value deep inside the structure:++>moveToAmsterdam :: Person -> Person+>moveToAmsterdam = set (city . place) "Amsterdam"++And now:++>ghci> moveToAmsterdam jan+>Person "Jan" 71 True (Place "Amsterdam" "The Netherlands" "Europe")++Composition is done using the @(`.`)@ operator which is part of the+"Control.Category" module. Make sure to import this module and hide the default+@(`.`)@, `id` function from the Haskell "Prelude".++-}++-- * Pure lenses.++ (:->)+, lens+, get+, set+, modify++-- * Views using @Applicative@.++{- |++Now, because Jan is an old guy, moving to another city is not a very easy task,+this really takes a while. It will probably take no less than two years before+he will actually be settled. To reflect this change it might be useful to have+a first class view on the `Person` datatype that only reveals the age and+city. This can be done by using a neat `Applicative` functor instance:++>import Control.Applicative++>ageAndCity :: Person :-> (Int, String)+>ageAndCity = Lens $+> (,) <$> fst `for` age+> <*> snd `for` city . place++Because the applicative type class on its own is not very capable of expressing+bidirectional relations, which we need for our lenses, the actual instance is+defined for an internal helper structure called `Point`. Points are a bit more+general than lenses. As you can see above, the `Label` constructor has to be+used to convert a `Point` back into a `Label`. The `for` function must be used+to indicate which partial destructor to use for which lens in the applicative+composition.++Now that we have an appropriate age+city view on the `Person` datatype (which+is itself a lens again), we can use the `modify` function to make Jan move to+Amsterdam over exactly two years:++>moveToAmsterdamOverTwoYears :: Person -> Person+>moveToAmsterdamOverTwoYears = modify ageAndCity (\(a, _) -> (a+2, "Amsterdam"))++>ghci> moveToAmsterdamOverTwoYears jan+>Person "Jan" 73 True (Place "Amsterdam" "The Netherlands" "Europe")++-}++, Lens (Lens)++-- * Working with bijections and isomorphisms.+-- +-- | This package contains a bijection datatype that encodes bidirectional+-- functions. Just like lenses, bijections can be composed using the+-- "Control.Category" type class. Bijections can be used to change the type of+-- a lens. The `Iso` type class, which can be seen as a bidirectional functor,+-- can be used to apply bijections to lenses.+-- +-- For example, when we want to treat the age of a person as a string we can do+-- the following:+-- +-- > ageAsString :: Person :-> String+-- > ageAsString = Bij show read `iso` age++, Bijection (..)+, Iso (..)+, for++-- * Derive labels using Template Haskell.+--+-- | We can either derive labels with or without type signatures. In the case+-- of multi-constructor datatypes some fields might not always be available and+-- the derived labels will be partial. Partial labels are provided with an+-- additional type context that forces them to be only usable using the+-- functions from "Data.Label.Maybe".++, mkLabels+, mkLabel+, mkLabelsWith+, mkLabelsMono+, mkLabelsNoTypes+)+where++import Data.Label.Abstract (Bijection(..), Iso(..), for, Lens(..))+import Data.Label.Pure+import Data.Label.Derive+
+ src/Data/Label/Abstract.hs view
@@ -0,0 +1,133 @@+{-# LANGUAGE+ TypeOperators+ , Arrows+ , TupleSections+ , FlexibleInstances+ , MultiParamTypeClasses+ #-}+module Data.Label.Abstract where++import Control.Arrow+import Prelude hiding ((.), id)+import Control.Applicative+import Control.Category++{-# INLINE _modify #-}+{-# INLINE lens #-}+{-# INLINE get #-}+{-# INLINE set #-}+{-# INLINE modify #-}+{-# INLINE bimap #-}+{-# INLINE for #-}+{-# INLINE liftBij #-}++-- | Abstract Point datatype. The getter and setter functions work in some+-- arrow.++data Point arr f i o = Point+ { _get :: f `arr` o+ , _set :: (i, f) `arr` f+ }++-- | Modification as a compositon of a getter and setter. Unfortunately,+-- `ArrowApply' is needed for this composition.++_modify :: ArrowApply arr => Point arr f i o -> (o `arr` i, f) `arr` f+_modify l = proc (m, f) -> do i <- m . _get l -<< f; _set l -< (i, f)++-- | Abstract Lens datatype. The getter and setter functions work in some+-- arrow. Arrows allow for effectful lenses, for example, lenses that might+-- fail or use state.++newtype Lens arr f a = Lens { unLens :: Point arr f a a }++-- | Create a lens out of a getter and setter.++lens :: (f `arr` a) -> ((a, f) `arr` f) -> Lens arr f a+lens g s = Lens (Point g s)++-- | Get the getter arrow from a lens.++get :: Arrow arr => Lens arr f a -> f `arr` a+get = _get . unLens++-- | Get the setter arrow from a lens.++set :: Arrow arr => Lens arr f a -> (a, f) `arr` f+set = _set . unLens++-- | Get the modifier arrow from a lens.++modify :: ArrowApply arr => Lens arr f o -> (o `arr` o, f) `arr` f+modify = _modify . unLens++instance ArrowApply arr => Category (Lens arr) where+ id = lens id (arr fst)+ Lens a . Lens b = lens (_get a . _get b) (_modify b . first (curryA (_set a)))+ where curryA f = arr (\i -> f . arr (i,))+ {-# INLINE id #-}+ {-# INLINE (.) #-}++instance Arrow arr => Functor (Point arr f i) where+ fmap f x = Point (arr f . _get x) (_set x)+ {-# INLINE fmap #-}++instance Arrow arr => Applicative (Point arr f i) where+ pure a = Point (arr (const a)) (arr snd)+ a <*> b = Point (arr app . (_get a &&& _get b)) (_set b . (arr fst &&& _set a))+ {-# INLINE pure #-}+ {-# INLINE (<*>) #-}++-- | Make a 'Point' diverge in two directions.++bimap :: Arrow arr => (o' `arr` o) -> (i `arr` i') -> Point arr f i' o' -> Point arr f i o+bimap f g l = Point (f . _get l) (_set l . first g)++infix 8 `for`++for :: Arrow arr => (i `arr` o) -> Lens arr f o -> Point arr f i o+for p = bimap id p . unLens++-- | The bijections datatype, an arrow that works in two directions. ++infix 8 `Bij`++data Bijection arr a b = Bij { fw :: a `arr` b, bw :: b `arr` a }++-- | Bijections as categories.++instance Category arr => Category (Bijection arr) where+ id = Bij id id+ Bij a b . Bij c d = a . c `Bij` d . b+ {-# INLINE id #-}+ {-# INLINE (.) #-}++-- | Lifting 'Bijection's.++liftBij :: Functor f => Bijection (->) a b -> Bijection (->) (f a) (f b)+liftBij a = fmap (fw a) `Bij` fmap (bw a)++-- | The isomorphism type class is like a `Functor' but works in two directions.++infixr 8 `iso`++class Iso arr f where+ iso :: Bijection arr a b -> f a `arr` f b++-- | Flipped isomorphism.++osi :: Iso arr f => Bijection arr b a -> f a `arr` f b+osi (Bij a b) = iso (Bij b a)++-- | We can diverge 'Lens'es using an isomorphism.++instance Arrow arr => Iso arr (Lens arr f) where+ iso bi = arr ((\a -> lens (fw bi . _get a) (_set a . first (bw bi))) . unLens)+ {-# INLINE iso #-}++-- | We can diverge 'Bijection's using an isomorphism.++instance Arrow arr => Iso arr (Bijection arr a) where+ iso = arr . (.)+ {-# INLINE iso #-}+
+ src/Data/Label/Derive.hs view
@@ -0,0 +1,218 @@+{-# OPTIONS -fno-warn-orphans #-}+{-# LANGUAGE+ TemplateHaskell+ , OverloadedStrings+ , FlexibleContexts+ , FlexibleInstances+ , TypeOperators+ , CPP+ #-}+module Data.Label.Derive+( mkLabels+, mkLabel+, mkLabelsWith+, mkLabelsMono+, mkLabelsNoTypes+) where++import Control.Arrow+import Control.Category+import Control.Monad+import Data.Char+import Data.Function (on)+import Data.Label.Abstract+import Data.Label.Pure ((:->))+import Data.Label.Maybe ((:~>))+import Data.List+import Data.Ord+import Data.String+import Language.Haskell.TH+import Language.Haskell.TH.Syntax+import Prelude hiding ((.), id)++-- Throw a fclabels specific error.++fclError :: String -> a+fclError err = error ("Data.Label.Derive: " ++ err)++-- | Derive lenses including type signatures for all the record selectors for a+-- collection of datatypes. The types will be polymorphic and can be used in an+-- arbitrary context.++mkLabels :: [Name] -> Q [Dec]+mkLabels = mkLabelsWith defaultMakeLabel++-- | Derive lenses including type signatures for all the record selectors in a+-- single datatype. The types will be polymorphic and can be used in an+-- arbitrary context.++mkLabel :: Name -> Q [Dec]+mkLabel = mkLabels . return++-- | Generate the label name from the record field name.+-- For instance, @drop 1 . dropWhile (/='_')@ creates a label @val@ from a+-- record @Rec { rec_val :: X }@.++mkLabelsWith :: (String -> String) -> [Name] -> Q [Dec]+mkLabelsWith makeLabel = liftM concat . mapM (derive1 makeLabel True False)++-- | Derive lenses including type signatures for all the record selectors in a+-- datatype. The signatures will be concrete and can only be used in the+-- appropriate context.++mkLabelsMono :: [Name] -> Q [Dec]+mkLabelsMono = liftM concat . mapM (derive1 defaultMakeLabel True True)++-- | Derive lenses without type signatures for all the record selectors in a+-- datatype.++mkLabelsNoTypes :: [Name] -> Q [Dec]+mkLabelsNoTypes = liftM concat . mapM (derive1 defaultMakeLabel False False)++-- Helpers to generate all labels for one datatype.++derive1 :: (String -> String) -> Bool -> Bool -> Name -> Q [Dec]+derive1 makeLabel signatures concrete datatype =+ do i <- reify datatype+ let -- Only process data and newtype declarations, filter out all+ -- constructors and the type variables.+ (tyname, cons, vars) =+ case i of+ TyConI (DataD _ n vs cs _) -> (n, cs, vs)+ TyConI (NewtypeD _ n vs c _) -> (n, [c], vs)+ _ -> fclError "Can only derive labels for datatypes and newtypes."++ -- We are only interested in lenses of record constructors.+ recordOnly = groupByCtor [ (f, n) | RecC n fs <- cons, f <- fs ]++ concat `liftM`+ mapM (derive makeLabel signatures concrete tyname vars (length cons))+ recordOnly++ where groupByCtor = map (\xs -> (fst (head xs), map snd xs))+ . groupBy ((==) `on` (fst3 . fst))+ . sortBy (comparing (fst3 . fst))+ where fst3 (a, _, _) = a++-- Generate the code for the labels.++-- | Generate a name for the label. If the original selector starts with an+-- underscore, remove it and make the next character lowercase. Otherwise,+-- add 'l', and make the next character uppercase.+defaultMakeLabel :: String -> String+defaultMakeLabel field =+ case field of+ '_' : c : rest -> toLower c : rest+ f : rest -> 'l' : toUpper f : rest+ n -> fclError ("Cannot derive label for record selector with name: " ++ n)++derive :: (String -> String)+ -> Bool -> Bool -> Name -> [TyVarBndr] -> Int+ -> (VarStrictType, [Name]) -> Q [Dec]+derive makeLabel signatures concrete tyname vars total ((field, _, fieldtyp), ctors) =+ do (sign, body) <-+ if length ctors == total+ then function derivePureLabel+ else function deriveMaybeLabel++ return $+ if signatures+ then [sign, inline, body]+ else [inline, body]++ where++ -- Generate an inline declaration for the label.+ --+ -- Type of InlineSpec removed in TH-2.8.0 (GHC 7.6)+#if MIN_VERSION_template_haskell(2,8,0)+ inline = PragmaD (InlineP labelName Inline FunLike (FromPhase 0))+#else+ inline = PragmaD (InlineP labelName (InlineSpec True True (Just (True, 0))))+#endif+ labelName = mkName (makeLabel (nameBase field))++ -- Build a single record label definition for labels that might fail.+ deriveMaybeLabel = (if concrete then mono else poly, body)+ where+ mono = forallT prettyVars (return []) [t| $(inputType) :~> $(return prettyFieldtyp) |]+ poly = forallT forallVars (return [])+ [t| (ArrowChoice $(arrow), ArrowZero $(arrow))+ => Lens $(arrow) $(inputType) $(return prettyFieldtyp) |]+ body = [| lens (fromRight . $(getter)) (fromRight . $(setter)) |]+ where+ getter = [| arr (\ p -> $(caseE [|p|] (cases (bodyG [|p|] ) ++ wild))) |]+ setter = [| arr (\(v, p) -> $(caseE [|p|] (cases (bodyS [|p|] [|v|]) ++ wild))) |]+ cases b = map (\ctor -> match (recP ctor []) (normalB b) []) ctors+ wild = [match wildP (normalB [| Left () |]) []]+ bodyS p v = [| Right $( record p field v ) |]+ bodyG p = [| Right $( varE field `appE` p ) |]++ -- Build a single record label definition for labels that cannot fail.+ derivePureLabel = (if concrete then mono else poly, body)+ where+ mono = forallT prettyVars (return []) [t| $(inputType) :-> $(return prettyFieldtyp) |]+ poly = forallT forallVars (return [])+ [t| Arrow $(arrow) => Lens $(arrow) $(inputType) $(return prettyFieldtyp) |]+ body = [| lens $(getter) $(setter) |]+ where+ getter = [| arr $(varE field) |]+ setter = [| arr (\(v, p) -> $(record [| p |] field [| v |])) |]++ -- Compute the type (including type variables of the record datatype.+ inputType = return $ foldr (flip AppT) (ConT tyname) (map tvToVarT (reverse prettyVars))++ -- Convert a type variable binder to a regular type variable.+ tvToVarT (PlainTV tv ) = VarT tv+ tvToVarT (KindedTV tv kind) = SigT (VarT tv) kind++ -- Prettify type variables.+ arrow = varT (mkName "arr")+ prettyVars = map prettyTyVar vars+ forallVars = PlainTV (mkName "arr") : prettyVars+ prettyFieldtyp = prettyType fieldtyp++ -- Q style record updating.+ record rec fld val = val >>= \v -> recUpdE rec [return (fld, v)]++ -- Build a function declaration with both a type signature and body.+ function (s, b) = liftM2 (,) + (sigD labelName s)+ (funD labelName [ clause [] (normalB b) [] ])++fromRight :: (ArrowChoice a, ArrowZero a) => a (Either b d) d+fromRight = zeroArrow ||| returnA++-------------------------------------------------------------------------------++-- Helper functions to prettify type variables.++prettyName :: Name -> Name+prettyName tv = mkName (takeWhile (/='_') (show tv))++prettyTyVar :: TyVarBndr -> TyVarBndr+prettyTyVar (PlainTV tv ) = PlainTV (prettyName tv)+prettyTyVar (KindedTV tv ki) = KindedTV (prettyName tv) ki++prettyType :: Type -> Type+prettyType (ForallT xs cx ty) = ForallT (map prettyTyVar xs) (map prettyPred cx) (prettyType ty)+prettyType (VarT nm ) = VarT (prettyName nm)+prettyType (AppT ty tx ) = AppT (prettyType ty) (prettyType tx)+prettyType (SigT ty ki ) = SigT (prettyType ty) ki+prettyType ty = ty++prettyPred :: Pred -> Pred+prettyPred (ClassP nm tys) = ClassP (prettyName nm) (map prettyType tys)+prettyPred (EqualP ty tx ) = EqualP (prettyType ty) (prettyType tx)++-- IsString instances for TH types.++instance IsString Exp where+ fromString = VarE . mkName++instance IsString (Q Pat) where+ fromString = varP . mkName++instance IsString (Q Exp) where+ fromString = varE . mkName+
+ src/Data/Label/Maybe.hs view
@@ -0,0 +1,75 @@+{-# LANGUAGE TypeOperators, TupleSections #-}+module Data.Label.Maybe+( (:~>)+, lens+, get+, set+, set'+, modify+, modify'+, embed+)+where++import Control.Arrow+import Control.Category+import Control.Monad.Identity+import Control.Monad.Trans.Maybe+import Data.Maybe+import Prelude hiding ((.), id)+import qualified Data.Label.Abstract as A++type MaybeLens f a = A.Lens (Kleisli (MaybeT Identity)) f a++-- | Lens type for situations in which the accessor functions can fail. This is+-- useful, for example, when accessing fields in datatypes with multiple+-- constructors.++type f :~> a = MaybeLens f a++run :: Kleisli (MaybeT Identity) f a -> f -> Maybe a+run l = runIdentity . runMaybeT . runKleisli l++-- | Create a lens that can fail from a getter and a setter that can themselves+-- potentially fail.++lens :: (f -> Maybe a) -> (a -> f -> Maybe f) -> f :~> a+lens g s = A.lens (kl g) (kl (uncurry s))+ where kl a = Kleisli (MaybeT . Identity . a)++-- | Getter for a lens that can fail. When the field to which the lens points+-- is not accessible the getter returns 'Nothing'.++get :: (f :~> a) -> f -> Maybe a+get l = run (A.get l)++-- | Setter for a lens that can fail. When the field to which the lens points+-- is not accessible this function returns 'Nothing'.++set :: f :~> a -> a -> f -> Maybe f+set l v = run (A.set l . arr (v,))++-- | Like 'set' but return behaves like the identity function when the field+-- could not be set.++set' :: (f :~> a) -> a -> f -> f+set' l v f = f `fromMaybe` set l v f++-- | Modifier for a lens that can fail. When the field to which the lens points+-- is not accessible this function returns 'Nothing'.++modify :: (f :~> a) -> (a -> a) -> f -> Maybe f+modify l m = run (A.modify l . arr (arr m,))++-- | Like 'modify' but return behaves like the identity function when the field+-- could not be set.++modify' :: (f :~> a) -> (a -> a) -> f -> f+modify' l m f = f `fromMaybe` modify l m f++-- | Embed a pure lens that points to a `Maybe` field into a lens that might+-- fail.++embed :: A.Lens (->) f (Maybe a) -> f :~> a+embed l = lens (A.get l) (\a f -> Just (A.set l (Just a, f)))+
+ src/Data/Label/MaybeM.hs view
@@ -0,0 +1,31 @@+{-# LANGUAGE TypeOperators #-}+module Data.Label.MaybeM+(+-- * 'MonadState' lens operations.+ gets++-- * 'MonadReader' lens operations.+, asks+)+where++import Control.Monad+import Data.Label.Maybe ((:~>))+import qualified Control.Monad.Reader as M+import qualified Control.Monad.State as M+import qualified Data.Label.Maybe as L++-- | Get a value out of state, pointed to by the specified lens that might+-- fail. When the lens getter fails this computation will fall back to+-- `mzero'.++gets :: (M.MonadState f m, MonadPlus m) => (f :~> a) -> m a+gets l = (L.get l `liftM` M.get) >>= (mzero `maybe` return)++-- | Fetch a value, pointed to by a lens that might fail, out of a reader+-- environment. When the lens getter fails this computation will fall back to+-- `mzero'.++asks :: (M.MonadReader f m, MonadPlus m) => (f :~> a) -> m a+asks l = (L.get l `liftM` M.ask) >>= (mzero `maybe` return)+
+ src/Data/Label/Pure.hs view
@@ -0,0 +1,46 @@+{-# LANGUAGE TypeOperators #-}+module Data.Label.Pure+( (:->)+, lens+, get+, set+, modify+)+where++import qualified Data.Label.Abstract as A++type PureLens f a = A.Lens (->) f a++-- | Pure lens type specialized for pure accessor functions.++type (f :-> a) = PureLens f a++-- | Create a pure lens from a getter and a setter.+--+-- We expect the following law to hold:+--+-- > get l (set l a f) == a+--+-- Or, equivalently:+--+-- > set l (get l f) f == f++lens :: (f -> a) -> (a -> f -> f) -> f :-> a+lens g s = A.lens g (uncurry s)++-- | Getter for a pure lens.++get :: (f :-> a) -> f -> a+get = A.get++-- | Setter for a pure lens.++set :: (f :-> a) -> a -> f -> f+set = curry . A.set++-- | Modifier for a pure lens.++modify :: (f :-> a) -> (a -> a) -> f -> f+modify = curry . A.modify+
+ src/Data/Label/PureM.hs view
@@ -0,0 +1,72 @@+{-# LANGUAGE TypeOperators #-}+module Data.Label.PureM+(+-- * 'MonadState' lens operations.+ gets+, puts+, modify+, modifyAndGet+, (=:)+, (=.)++-- * 'MonadReader' lens operations.+, asks+, local+)+where++import Control.Monad+import Data.Label.Pure ((:->))+import qualified Control.Monad.Reader as M+import qualified Control.Monad.State as M+import qualified Data.Label.Pure as L++-- | Get a value out of the state, pointed to by the specified lens.++gets :: M.MonadState s m => s :-> a -> m a+gets = M.gets . L.get++-- | Set a value somewhere in the state, pointed to by the specified lens.++puts :: M.MonadState s m => s :-> a -> a -> m ()+puts l = M.modify . L.set l++-- | Modify a value with a function somewhere in the state, pointed to by the+-- specified lens.++modify :: M.MonadState s m => s :-> a -> (a -> a) -> m ()+modify l = M.modify . L.modify l++-- | Alias for `puts' that reads like an assignment.++infixr 2 =:+(=:) :: M.MonadState s m => s :-> a -> a -> m ()+(=:) = puts++-- | Alias for `modify' that reads more or less like an assignment.++infixr 2 =.+(=.) :: M.MonadState s m => s :-> a -> (a -> a) -> m ()+(=.) = modify++-- | Fetch a value pointed to by a lens out of a reader environment.++asks :: M.MonadReader r m => (r :-> a) -> m a+asks = M.asks . L.get++-- | Execute a computation in a modified environment. The lens is used to+-- point out the part to modify.++local :: M.MonadReader r m => (r :-> b) -> (b -> b) -> m a -> m a+local l f = M.local (L.modify l f)++-- | Modify a value with a function somewhere in the state, pointed to by the+-- specified lens. Additionally return a separate value based on the+-- modification.++modifyAndGet :: M.MonadState s m => (s :-> a) -> (a -> (b, a)) -> m b+modifyAndGet l f =+ do (b, a) <- f `liftM` gets l+ puts l a+ return b+
src/Test/API.hs view
@@ -1,9 +1,9 @@ module Test.API where import Web.VKHS-import Text.Printf -- | Almost working example. Just set correct values for client_id/login/password+print_user_info :: IO () print_user_info = do let e = env "3213232" "user@example.com" "password" [Photos,Audio,Groups] r <- login e{verbose = Debug}
+ src/VKNews.hs view
@@ -0,0 +1,133 @@+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}++module Main where++import Control.Applicative+import Control.Concurrent (threadDelay)+import Control.Monad.Trans+import Control.Monad.Reader+import Control.Monad.State+import Control.Monad.Error+import Control.Monad+import Data.Aeson+import Data.Maybe+import Data.Either+import Data.Monoid+import Data.Time.Clock+import Options.Applicative+import System.Environment+import System.Exit+import System.IO+import Text.Printf+import Text.RegexPR+import Web.VKHS as VK hiding (api,api')+import Web.VKHS.API.Monad as VK++data Options = Options+ { verb :: Verbosity+ , application_id :: String+ , access_token :: String+ , vk_poll_interval_sec :: Int+ , username :: String+ , password :: String+ -- , vk_group_id :: String+ } deriving(Show)++data Pirozhok = Pirozhok+ { plines :: String+ , pdate :: UTCTime+ }++pprint p = liftIO $ do+ -- putStrLn (show $ pdate p)+ putStr (plines p)++type PState a = StateT UTCTime (VKAPI IO) a++instance Error (String,Maybe a) where+ strMsg s = (s,Nothing)++pirozhok d' wr@(WR _ _ _ t _) = Pirozhok <$> poetry <*> date+ where+ poetry = txt >>= nonempty >>= four >>=+ maxlet >>= (pure . unlines)+ date = check (publishedAt wr) where+ check d | d <= d' = oops $ "older than " ++ (show d')+ | otherwise = pure d+ txt = pure $ lines $ gsubRegexPR "<br>" "\n" $ takeWhile (/= '©') t+ nonempty ls = pure $ filter (/=[]) ls+ four ls | length ls >4 = oops "more than 4 lines"+ | otherwise = pure ls+ maxlet ls | (sum $ map length ls) > 250 = oops "more than 250 letters"+ | otherwise = pure ls+ oops s = throwError (s, Just wr)++env_var_name = "VKNEWS_ACCESS_TOKEN"++opts at = Options+ <$> flag Normal Debug (long "verbose" <> help "Be verbose")+ <*> strOption (long "application-id" <> short 'a' <> value vkhs_app_id <> help (printf "Application ID (can be set via %s)" env_var_name))+ <*> strOption (long "access-token" <> short 't' <> value at <> help "Access token")+ <*> option (long "poll-interval" <> short 'i' <> value 20 <> help "Poll interval [sec]")+ <*> argument str (metavar "USERNAME" <> help "User name")+ <*> strOption (metavar "STR" <> long "password" <> short 'p' <> value "-" <> help "Password")+ -- <*> argument str (metavar "GROUPID" <> help "Vkontakte ID of the group to read the news from")+ where+ vkhs_app_id = "3128877"++pirozhki = do+ Response (SL len ws) <- lift $ VK.api' "wall.get" [("owner_id",gid_piro)]+ d <- get+ e <- ask+ let ps = map (pirozhok d) ws+ let d' = maxtime d (map pdate $ rights ps)+ forM (lefts ps) $ \(s,wr) -> do+ when (verbose e >= Trace) $ do+ liftIO $ hPutStrLn stderr $ printf "Rejecting record %s. Reason: %s" (maybe "?" (show . wid) wr) s+ when (d' > d) $ do+ when (verbose e >= Trace) $ do+ liftIO $ hPutStrLn stderr $ printf "Updating time to %s" (show d')+ (put d')+ return (rights ps)+ where+ maxtime d [] = d+ maxtime d ps = maximum ps+ gid_piro = "-28122932"++cmd :: Options -> IO ()+cmd (Options v apid at pollint u pass) = run $ do+ forever $ do+ ps <- pirozhki+ forM ps $ \p -> do+ pprint p+ pmsg []+ sleep_sec pollint++ where++ run vk = do+ t <- getCurrentTime+ let e = (VK.env apid u pass VK.allAccess) { verbose = v }+ let ma = runStateT vk t+ r <- runVKAPI ma ([],[],[]) e+ case r of+ Left er -> do+ perror (show er)+ exitFailure+ Right ((a,_),_) -> return a++ perror s = liftIO $ hPutStrLn stderr s++ pmsg s = liftIO $ putStrLn s++ sleep_sec s = liftIO $ threadDelay (1000 * 1000 * s)+++main :: IO ()+main = do+ hSetBuffering stdout NoBuffering+ hSetBuffering stderr NoBuffering+ at <- fromMaybe [] <$> lookupEnv env_var_name+ execParser (info (opts at) idm) >>= cmd+
src/VKQ.hs view
@@ -6,13 +6,9 @@ module Main where import Control.Monad-import Control.Monad.Error import Data.Aeson as A import Data.Label.Abstract-import Data.Typeable-import Data.Data import Data.List-import Data.Maybe import qualified Data.ByteString as BS import qualified Data.ByteString.UTF8 as U import Network.Protocol.Uri.Query@@ -29,7 +25,7 @@ import Web.VKHS import Web.VKHS.Curl import Web.VKHS.API.Aeson as A-import Web.VKHS.API as STR+import Web.VKHS.API.Base as STR data Options = Options { verb :: Verbosity@@ -65,6 +61,7 @@ , output_format :: String , out_dir :: String , records_id :: [String]+ , skip_existing :: Bool } deriving(Show) data UserOptions = UO@@ -74,59 +71,60 @@ data WallOptions = WO { accessToken_w :: String- , oid :: String+ , woid :: String } deriving(Show) loginOptions :: Parser CmdOptions loginOptions = Login <$> (LoginOptions- <$> strOption (metavar "APPID" & short 'a' & value "3128877" & help "Application ID, defaults to VKHS" )- <*> argument str (metavar "USER" & help "User name or email")- <*> argument str (metavar "PASS" & help "User password"))+ <$> strOption (metavar "APPID" <> short 'a' <> value "3128877" <> help "Application ID, defaults to VKHS" )+ <*> argument str (metavar "USER" <> help "User name or email")+ <*> argument str (metavar "PASS" <> help "User password")) opts m =- let access_token_flag = strOption (short 'a' & m & metavar "ACCESS_TOKEN" &+ let access_token_flag = strOption (short 'a' <> m <> metavar "ACCESS_TOKEN" <> help "Access token. Honores VKQ_ACCESS_TOKEN environment variable") in Options- <$> flag Normal Debug (long "verbose" & help "Be verbose")+ <$> flag Normal Debug (long "verbose" <> help "Be verbose") <*> subparser ( command "login" (info loginOptions- ( progDesc "Login and print access token (also prints user_id and expiriration time)" ))- & command "call" (info (Call <$> (CO+ ( progDesc "Login and print access token (also prints user_id and expiration time)" ))+ <> command "call" (info (Call <$> (CO <$> access_token_flag- <*> switch (long "preparse" & short 'p' & help "Preparse into Aeson format")- <*> argument str (metavar "METHOD" & help "Method name")- <*> argument str (metavar "PARAMS" & help "Method arguments, KEY=VALUE[,KEY2=VALUE2[,,,]]")))+ <*> switch (long "preparse" <> short 'p' <> help "Preparse into Aeson format")+ <*> argument str (metavar "METHOD" <> help "Method name")+ <*> argument str (metavar "PARAMS" <> help "Method arguments, KEY=VALUE[,KEY2=VALUE2[,,,]]"))) ( progDesc "Call VK API method" ))- & command "music" (info ( Music <$> (MO+ <> command "music" (info ( Music <$> (MO <$> access_token_flag- <*> switch (long "list" & short 'l' & help "List music files")+ <*> switch (long "list" <> short 'l' <> help "List music files") <*> strOption ( metavar "STR"- & long "query" & short 'q' & value [] & help "Query string")+ <> long "query" <> short 'q' <> value [] <> help "Query string") <*> strOption ( metavar "FORMAT"- & short 'f'- & value "%o_%i %u\t%t"- & help "Listing format, supported tags: %i %o %a %t %d %u"+ <> short 'f'+ <> value "%o_%i %u\t%t"+ <> help "Listing format, supported tags: %i %o %a %t %d %u" ) <*> strOption ( metavar "FORMAT"- & short 'F'- & value "%a - %t"- & help "FileName format, supported tags: %i %o %a %t %d %u"+ <> short 'F'+ <> value "%a - %t"+ <> help "FileName format, supported tags: %i %o %a %t %d %u" )- <*> strOption (metavar "DIR" & short 'o' & help "Output directory" & value "")- <*> arguments str (metavar "RECORD_ID" & help "Download records")+ <*> strOption (metavar "DIR" <> short 'o' <> help "Output directory" <> value "")+ <*> arguments str (metavar "RECORD_ID" <> help "Download records")+ <*> flag False True (long "skip-existing" <> help "Don't download existing files") )) ( progDesc "List or download music files"))- & command "user" (info ( UserQ <$> (UO+ <> command "user" (info ( UserQ <$> (UO <$> access_token_flag- <*> strOption (long "query" & short 'q' & help "String to query")+ <*> strOption (long "query" <> short 'q' <> help "String to query") )) ( progDesc "Extract various user information"))- & command "wall" (info ( WallQ <$> (WO+ <> command "wall" (info ( WallQ <$> (WO <$> access_token_flag- <*> strOption (long "id" & short 'i' & help "Owner id")+ <*> strOption (long "id" <> short 'i' <> help "Owner id") )) ( progDesc "Extract wall information")) )@@ -136,7 +134,7 @@ r <- A.api' a b c case r of Right x -> return x- Left e -> hPutStrLn stderr e >> exitFailure+ Left e -> hPutStrLn stderr (show e) >> exitFailure api_str :: Env CallEnv -> String -> [(String,String)] -> IO String api_str a b c = do@@ -165,7 +163,7 @@ main :: IO () main = do m <- maybe (idm) (value) <$> lookupEnv "VKQ_ACCESS_TOKEN"- execParser (info (opts m) idm) >>= cmd+ execParser (info (helper <*> opts m) (fullDesc <> header "Vkontakte social network tool")) >>= cmd cmd :: Options -> IO () @@ -173,13 +171,13 @@ cmd (Options v (Login (LoginOptions a u p))) = do let e = (env a u p allAccess) { verbose = v } ea <- login e- ifeither ea errexit $ \(at,uid,expin) -> do- printf "%s %s %s\n" at uid expin+ ifeither ea errexit $ \(at,usrid,expin) -> do+ printf "%s %s %s\n" at usrid expin -- Call user-specified API-cmd (Options v (Call (CO act pp mn args))) = do+cmd (Options v (Call (CO act pp mn as))) = do let e = (envcall act) { verbose = v }- let s = fw (keyValues "," "=") args+ let s = fw (keyValues "," "=") as case pp of False -> do ea <- api_str e mn s@@ -189,7 +187,7 @@ putStrLn $ PP.ppShow ea -- Query audio files-cmd (Options v (Music mo@(MO act _ q@(_:_) fmt _ _ _))) = do+cmd (Options v (Music (MO act _ q@(_:_) fmt _ _ _ _))) = do let e = (envcall act) { verbose = v } Response (SL len ms) <- api_ e "audio.search" [("q",q)] forM_ ms $ \m -> do@@ -197,28 +195,33 @@ printf "total %d\n" len -- List audio files -cmd (Options v (Music mo@(MO act True [] fmt _ _ _))) = do+cmd (Options v (Music (MO act True [] fmt _ _ _ _))) = do let e = (envcall act) { verbose = v } (Response ms) <- api_ e "audio.get" [] forM_ ms $ \m -> do printf "%s\n" (mr_format fmt m)-cmd (Options v (Music mo@(MO act False [] _ ofmt odir []))) = do+cmd (Options _ (Music (MO _ False [] _ _ _ [] _))) = do errexit "Music record ID is not specified (see --help)" -- Download audio files-cmd (Options v (Music mo@(MO act False [] _ ofmt odir rid))) = do+cmd (Options v (Music (MO act False [] _ ofmt odir rid sk))) = do let e = (envcall act) { verbose = v } Response ms <- api_ e "audio.getById" [("audios", concat $ intersperse "," rid)] forM_ ms $ \m -> do- (fp, h) <- openFileMR odir ofmt m- r <- vk_curl_file e (url m) $ \ bs -> do- BS.hPut h bs- checkRight r- printf "%d_%d\n" (owner_id m) (aid m)- printf "%s\n" (title m)- printf "%s\n" fp+ (fp, mh) <- openFileMR odir sk ofmt m+ case mh of+ Just h -> do+ r <- vk_curl_file e (url m) $ \ bs -> do+ BS.hPut h bs+ checkRight r+ printf "%d_%d\n" (owner_id m) (aid m)+ printf "%s\n" (title m)+ printf "%s\n" fp+ Nothing -> do+ hPutStrLn stderr (printf "File %s already exist, skipping" fp)+ return () -cmd (Options v (UserQ uo@(UO act qs))) = do+cmd (Options v (UserQ (UO act qs))) = do let e = (envcall act) { verbose = v } print qs ea <- A.api e "users.search" [("q",qs),("fields","uid,first_name,last_name,photo,education")]@@ -227,9 +230,9 @@ -- processUQ uo ae -- List wall messages-cmd (Options v (WallQ mo@(WO act wid))) = do+cmd (Options v (WallQ (WO act oid))) = do let e = (envcall act) { verbose = v }- (Response (SL len ws)) <- api_ e "wall.get" [("owner_id",wid)]+ (Response (SL len ws)) <- api_ e "wall.get" [("owner_id",oid)] forM_ ws $ \w -> do putStrLn (show $ wdate w) putStrLn (wtext w)@@ -237,19 +240,25 @@ type NameFormat = String -openFileMR :: FilePath -> NameFormat -> MusicRecord -> IO (FilePath, Handle)-openFileMR [] _ m = do+-- Open file. Return filename and handle. Don't open file if it exists+openFileMR :: FilePath -> Bool -> NameFormat -> MusicRecord -> IO (FilePath, Maybe Handle)+openFileMR [] _ _ m = do let (_,ext) = splitExtension (url m) temp <- getTemporaryDirectory (fp,h) <- openBinaryTempFile temp ("vkqmusic"++ext)- return (fp,h)-openFileMR dir fmt m = do+ return (fp, Just h)+openFileMR dir sk fmt m = do let (_,ext) = splitExtension (url m) let name = mr_format fmt m- let name' = replaceExtension name ext+ let name' = replaceExtension name (takeWhile (/='?') ext) let fp = (dir </> name') - h <- openBinaryFile fp WriteMode- return (fp,h)+ e <- doesFileExist fp+ case (e && sk) of+ True -> do+ return (fp,Nothing)+ False -> do+ h <- openBinaryFile fp WriteMode+ return (fp,Just h) -- data Collection a = MC { -- response :: [a]
src/Web/VKHS/API.hs view
@@ -1,44 +1,6 @@ module Web.VKHS.API- ( api- , envcall- ) where--import Control.Monad-import Control.Monad.Trans-import Control.Monad.Writer-import Control.Monad.Error--import Data.Label-import qualified Data.ByteString.UTF8 as U-import qualified Data.ByteString as BS--import Network.Curlhs.Core--import Network.Protocol.Http-import Network.Protocol.Uri-import Network.Protocol.Uri.Query--import Text.Printf--import Web.VKHS.Types-import Web.VKHS.Curl--envcall :: String -> Env CallEnv-envcall at = mkEnv (CallEnv at)+ ( module Web.VKHS.API.Aeson+ ) where --- | Invoke the request. Return answer (normally, string representation of--- JSON data). See documentation:------ <http://vk.com/developers.php?oid=-1&p=%D0%9E%D0%BF%D0%B8%D1%81%D0%B0%D0%BD%D0%B8%D0%B5_%D0%BC%D0%B5%D1%82%D0%BE%D0%B4%D0%BE%D0%B2_API>-api :: Env CallEnv- -- ^ the VKHS environment- -> String- -- ^ API method name- -> [(String, String)]- -- ^ API method parameters (name-value pairs)- -> IO (Either String BS.ByteString)-api e mn mp =- let uri = showUri $ (\f -> f $ toUri $ printf "https://api.vk.com/method/%s" mn) $- set query $ bw params (("access_token",(access_token . sub) e):mp)- in vk_curl_payload e (tell [CURLOPT_URL $ U.fromString uri])+import Web.VKHS.API.Aeson
src/Web/VKHS/API/Aeson.hs view
@@ -2,38 +2,45 @@ {-# LANGUAGE OverloadedStrings #-} module Web.VKHS.API.Aeson- ( api- , api'- , module Web.VKHS.API.Types- ) where+ ( api+ , api'+ , Base.envcall+ , APIError(..)+ , module Web.VKHS.API.Types+ ) where import Control.Applicative-import Control.Monad-import Control.Monad.Trans-import Control.Monad.Writer import Control.Monad.Error import Data.Aeson as A-import qualified Data.Aeson.Types as A-import Data.Aeson (FromJSON, (.:), (.:?))+import Data.Aeson.Types as A import Data.Aeson.Generic as AG import Data.ByteString.Lazy as BS-import Data.Typeable import Data.Data-import Data.Vector as V (head, tail, toList)-+import Data.Vector as V (head, tail)+import Text.Printf import Web.VKHS.Types import Web.VKHS.API.Types-import qualified Web.VKHS.API as Base+import qualified Web.VKHS.API.Base as Base +parseJSON_obj_error :: String -> A.Value -> A.Parser a+parseJSON_obj_error name o = fail $+ printf "parseJSON: %s expects object, got %s" (show name) (show o)++parseJSON_arr_error :: String -> A.Value -> A.Parser a+parseJSON_arr_error name o = fail $+ printf "parseJSON: %s expects array, got %s" (show name) (show o)+ instance (FromJSON a) => FromJSON (Response a) where parseJSON (A.Object v) = do a <- v .: "response" x <- A.parseJSON a return (Response x)+ parseJSON o = parseJSON_obj_error "Response" o -parseGeneric a =- case AG.fromJSON a of+parseGeneric :: (Data a) => A.Value -> A.Parser a+parseGeneric val =+ case AG.fromJSON val of A.Success a -> return a A.Error s -> fail $ "parseGeneric fails:" ++ s @@ -47,20 +54,40 @@ <*> (o .: "from_id") <*> (o .: "text") <*> (o .: "date")+ parseJSON o = parseJSON_obj_error "WallRecord" o instance (FromJSON a) => FromJSON (SizedList [a]) where parseJSON (A.Array v) = do n <- A.parseJSON (V.head v) t <- A.parseJSON (A.Array (V.tail v)) return (SL n t)+ parseJSON o = parseJSON_arr_error "SizedList" o -api' :: (A.FromJSON a) => Env CallEnv -> String -> [(String,String)] -> IO (Either String a)-api' e mn mp = runErrorT $ do- e <- ErrorT (Base.api e mn mp)- let check (Just x) = return x- check (Nothing) = fail $ "AESON: error parsing JSON: " ++ show e- check $ A.decode $ BS.fromStrict e+instance FromJSON RespError where+ parseJSON (Object o) = do+ (Object e) <- o .: "error"+ ER <$> (e .: "error_code")+ <*> (e .: "error_msg")+ parseJSON o = parseJSON_obj_error "RespError" o -api :: Env CallEnv -> String -> [(String,String)] -> IO (Either String A.Value)+data APIError = APIE_resp RespError | APIE_other String | APIE_badAccessToken+ deriving(Show)++instance Error APIError where+ strMsg x = APIE_other x++api' :: (A.FromJSON a) => Env CallEnv -> String -> [(String,String)] -> IO (Either APIError a)+api' env mn mp+ | Prelude.null ((access_token . sub) env) = return (Left APIE_badAccessToken)+ | otherwise = runErrorT $ do+ e <- BS.fromStrict <$> ErrorT (either (Left . APIE_other) (Right . id) <$> (Base.api env mn mp))+ case (A.decode e) of+ Just x -> return x+ Nothing -> do+ case (A.decode e) of+ Just x -> throwError (APIE_resp x)+ Nothing -> throwError $ APIE_other $ "AESON: error parsing JSON: " ++ show e++api :: Env CallEnv -> String -> [(String,String)] -> IO (Either APIError A.Value) api = api'
+ src/Web/VKHS/API/Monad.hs view
@@ -0,0 +1,76 @@+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}++module Web.VKHS.API.Monad+ ( api+ , api'+ , runVKAPI+ , VKAPI(..)+ , module Web.VKHS.API.Types+ ) where++import Control.Applicative+import Control.Monad.Error+import Control.Monad.State+import Control.Monad.Reader+import Control.Concurrent (threadDelay)+import Data.Aeson as A++import Web.VKHS.API.Types+import qualified Web.VKHS.API.Aeson as VK+import qualified Web.VKHS.Login as VK+import Web.VKHS as VK (callEnv, Env(..), LoginEnv(..), AccessToken, APIError(..))++newtype VKAPI m a = VKAPI { unVKAPI :: ReaderT (Env LoginEnv) (StateT AccessToken (ErrorT APIError m)) a }+ deriving(Monad, Applicative, Functor, MonadIO, MonadState AccessToken, MonadReader (Env LoginEnv), MonadError APIError)++runVKAPI :: (MonadIO m) => VKAPI m a -> VK.AccessToken -> Env LoginEnv -> m (Either VK.APIError (a,AccessToken))+runVKAPI m at e = runErrorT (runStateT (runReaderT (unVKAPI m) e) at)++shallTryRelogin :: APIError -> Bool+shallTryRelogin (APIE_other _) = False+shallTryRelogin _ = True++-- TODO: report whole error stack+apiRetryWrapper :: (A.FromJSON a, MonadIO m) => Int -> String -> [(String,String)] -> VKAPI m a+apiRetryWrapper nr name args = do+ e <- ask+ (at,_,_) <- get+ r <- liftIO $ VK.api' (callEnv e at) name args+ case (nr,r) of+ (0, Left er) -> throwError er+ (x, Left er)+ | shallTryRelogin er -> do+ res <- liftIO $ VK.login e+ case res of+ Left err -> throwError (VK.APIE_other err)+ Right at' -> do+ put at' >> apiRetryWrapper (x-1) name args+ | otherwise -> throwError er+ (_, Right a) -> return a++apiForewerWrapper :: (A.FromJSON a, MonadIO m) => String -> [(String,String)] -> VKAPI m a+apiForewerWrapper name args = do+ e <- ask+ let call_api = do+ (at,_,_) <- get+ r <- liftIO $ VK.api' (callEnv e at) name args+ case r of+ (Left _) -> do_login+ (Right a) -> return a+ do_login = do+ sleep 3+ r <- liftIO $ VK.login e+ case r of+ Left _ -> do_login+ Right at' -> put at' >> call_api+ sleep x = + liftIO $ threadDelay (1000 * 1000 * x); -- convert sec to us+ call_api++api' :: (A.FromJSON a, MonadIO m) => String -> [(String,String)] -> VKAPI m a+api' = apiForewerWrapper++api :: (MonadIO m) => String -> [(String,String)] -> VKAPI m A.Value+api = api'+
src/Web/VKHS/API/Types.hs view
@@ -4,7 +4,8 @@ import Data.Typeable import Data.Data-import Data.Text+import Data.Time.Clock+import Data.Time.Clock.POSIX data Response a = Response a deriving(Show)@@ -33,13 +34,21 @@ , graduation :: Maybe Int } deriving(Show,Data,Typeable) - data WallRecord = WR { wid :: Int , to_id :: Int , from_id :: Int , wtext :: String , wdate :: Int+ } deriving(Show)++publishedAt :: WallRecord -> UTCTime+publishedAt wr = posixSecondsToUTCTime $ fromIntegral $ wdate wr+++data RespError = ER+ { error_code :: Int+ , error_msg :: String } deriving(Show)