packages feed

json-feed 1.0.9 → 1.0.10

raw patch · 9 files changed

+407/−482 lines, 9 filesdep ~aesondep ~basedep ~bytestringPVP ok

version bump matches the API change (PVP)

Dependency ranges changed: aeson, base, bytestring

API changes (from Hackage documentation)

Files

− CHANGELOG.markdown
@@ -1,7 +0,0 @@-# Change log--JSON Feed uses [Semantic Versioning][].-The change log is available through the [releases on GitHub][].--[Semantic Versioning]: http://semver.org/spec/v2.0.0.html-[releases on GitHub]: https://github.com/tfausak/json-feed/releases
README.markdown view
@@ -1,10 +1,9 @@ # JSON Feed -[![][build badge]][build status]+[![Travis CI](https://travis-ci.org/tfausak/json-feed.svg?branch=master)](https://travis-ci.org/tfausak/json-feed)+[![Hackage](https://img.shields.io/hackage/v/json-feed)](https://hackage.haskell.org/package/json-feed)+[![Stackage](https://www.stackage.org/package/json-feed/badge/nightly?label=stackage)](https://www.stackage.org/package/json-feed)  This is an implementation of the JSON Feed spec in Haskell. The spec is available at <https://jsonfeed.org>. JSON Feed is similar to the Atom and RSS feed formats, but it is serialized as JSON rather than XML.--[build badge]: https://travis-ci.org/tfausak/json-feed.svg?branch=master-[build status]: https://travis-ci.org/tfausak/json-feed
json-feed.cabal view
@@ -1,84 +1,71 @@-cabal-version: 1.12+name: json-feed+version: 1.0.10 --- This file has been generated from package.yaml by hpack version 0.33.0.------ see: https://github.com/sol/hpack------ hash: 0237ba25ba89ff1d202ca5905037a4f062aecc9f1cfabee0851b168cdcf9c31a+synopsis: JSON Feed+description:+  This is an implementation of the JSON Feed spec in Haskell. The spec is+  available at <https://jsonfeed.org>. JSON Feed is similar to the Atom and RSS+  feed formats, but it is serialized as JSON rather than XML. -name:           json-feed-version:        1.0.9-synopsis:       JSON Feed-description:    This is an implementation of the JSON Feed spec in Haskell. The spec is available at <https://jsonfeed.org>. JSON Feed is similar to the Atom and RSS feed formats, but it is serialized as JSON rather than XML.-category:       Web-homepage:       https://github.com/tfausak/json-feed#readme-bug-reports:    https://github.com/tfausak/json-feed/issues-maintainer:     Taylor Fausak-license:        MIT-license-file:   LICENSE.markdown-build-type:     Simple+build-type: Simple+cabal-version: >= 1.10+category: Web extra-source-files:-    CHANGELOG.markdown-    feeds/allenpike.com.json-    feeds/daringfireball.net.json-    feeds/flyingmeat.com.json-    feeds/hypercritical.co.json-    feeds/inessential.com.json-    feeds/jsonfeed.org.json-    feeds/manton.org.json-    feeds/maybepizza.com.json-    feeds/shapeof.com.json-    feeds/therecord.co.json-    feeds/timetable.manton.org.json-    package.yaml-    README.markdown-    stack.yaml+  README.markdown+  feeds/allenpike.com.json+  feeds/daringfireball.net.json+  feeds/flyingmeat.com.json+  feeds/hypercritical.co.json+  feeds/inessential.com.json+  feeds/jsonfeed.org.json+  feeds/manton.org.json+  feeds/maybepizza.com.json+  feeds/shapeof.com.json+  feeds/therecord.co.json+  feeds/timetable.manton.org.json+license-file: LICENSE.markdown+license: MIT+maintainer: Taylor Fausak  source-repository head-  type: git   location: https://github.com/tfausak/json-feed+  type: git  library-  exposed-modules:-      JsonFeed-  other-modules:-      Paths_json_feed-  hs-source-dirs:-      library-  ghc-options: -Weverything -Wno-implicit-prelude -Wno-safe -Wno-unsafe   build-depends:-      aeson >=0.11.2 && <0.12 || >=1.0.2 && <1.5-    , base >=4.9.0 && <4.15-    , bytestring >=0.10.8 && <0.11-    , mime-types >=0.1.0 && <0.2-    , network-uri >=2.6.1 && <2.8-    , tagsoup >=0.14 && <0.15-    , text >=1.2.2 && <1.3-    , time >=1.6.0 && <1.10-  if impl(ghc >= 8.8.1)+      aeson >= 0.11.2 && < 0.12 || >= 1.0.2 && < 1.6+    , base >= 4.9.0 && < 4.15+    , bytestring >= 0.10.8 && < 0.11+    , mime-types >= 0.1.0 && < 0.2+    , network-uri >= 2.6.1 && < 2.8+    , tagsoup >= 0.14 && < 0.15+    , text >= 1.2.2 && < 1.3+    , time >= 1.6.0 && < 1.10+  default-language: Haskell98+  exposed-modules: JsonFeed+  ghc-options:+    -Weverything+    -Wno-implicit-prelude+    -Wno-safe+    -Wno-unsafe+  hs-source-dirs: src/lib++  if impl(ghc >= 8.8)     ghc-options: -Wno-missing-deriving-strategies-  default-language: Haskell2010 +  if impl(ghc >= 8.10)+    ghc-options:+      -Wno-missing-safe-haskell-mode+      -Wno-prepositive-qualified-module+ test-suite test-  type: exitcode-stdio-1.0-  main-is: Main.hs-  other-modules:-      Paths_json_feed-  hs-source-dirs:-      tests-  ghc-options: -Weverything -Wno-implicit-prelude -Wno-safe -Wno-unsafe -rtsopts -threaded   build-depends:-      aeson >=0.11.2 && <0.12 || >=1.0.2 && <1.5-    , base >=4.9.0 && <4.15-    , bytestring >=0.10.8 && <0.11-    , filepath >=1.4.1 && <1.5-    , hspec >=2.2.3 && <2.8-    , json-feed-    , mime-types >=0.1.0 && <0.2-    , network-uri >=2.6.1 && <2.8-    , tagsoup >=0.14 && <0.15-    , text >=1.2.2 && <1.3-    , time >=1.6.0 && <1.10-  if impl(ghc >= 8.8.1)-    ghc-options: -Wno-missing-deriving-strategies-  default-language: Haskell2010+    base -any+    , bytestring -any+    , filepath >= 1.4.1 && < 1.5+    , hspec >= 2.2.3 && < 2.8+    , json-feed -any+  default-language: Haskell98+  hs-source-dirs: src/test+  main-is: Main.hs+  type: exitcode-stdio-1.0
− library/JsonFeed.hs
@@ -1,313 +0,0 @@-{-# LANGUAGE DeriveGeneric #-}---- | <https://jsonfeed.org>-module JsonFeed-  ( parseFeed-  , renderFeed-  -- * Types-  , Feed (..)-  , Author (..)-  , Item (..)-  , Attachment (..)-  , Hub (..)-  -- * Wrappers-  , Html (..)-  , Mime (..)-  , Url (..)-  ) where--import Data.Aeson (FromJSON, ToJSON, Value)-import Data.Aeson.Types (Options)-import Data.ByteString.Lazy (ByteString)-import Data.Text (Text)-import Data.Time (UTCTime)-import GHC.Generics (Generic)-import Network.Mime (MimeType)-import Network.URI (URI)-import Numeric.Natural (Natural)-import Text.HTML.TagSoup (Tag)--import qualified Data.Aeson as Json (eitherDecode, encode)-import qualified Data.Aeson.Types as Json-import qualified Data.List as List-import qualified Data.Text as Text-import qualified Data.Text.Encoding as Text-import qualified Network.URI as Uri-import qualified Text.HTML.TagSoup as Html---parseFeed :: ByteString -> Either String Feed-parseFeed = Json.eitherDecode---renderFeed :: Feed -> ByteString-renderFeed = Json.encode---data Feed = Feed-  { feedAuthor :: Maybe Author-  -- ^ The feed author. The author object has several members. These are all-  -- optional --- but if you provide an author object, then at least one is-  -- required.-  , feedDescription :: Maybe Text-  -- ^ Provides more detail, beyond the title, on what the feed is about. A-  -- feed reader may display this text.-  , feedExpired :: Maybe Bool-  -- ^ Says whether or not the feed is finished --- that is, whether or not it-  -- will ever update again. A feed for a temporary event, such as an instance-  -- of the Olympics, could expire. If the value is 'True', then it's expired.-  -- Any other value, or the absence of 'feedExpired', means the feed may-  -- continue to update.-  , feedFavicon :: Maybe Url-  -- ^ The URL of an image for the feed suitable to be used in a source list.-  -- It should be square and relatively small, but not smaller than 64 x 64 (so-  -- that it can look good on retina displays). As with 'feedIcon', this image-  -- should use transparency where appropriate, since it may be rendered on a-  -- non-white background.-  , feedFeedUrl :: Maybe Url-  -- ^ The URL of the feed, and serves as the unique identifier for the feed.-  -- As with 'feedHomePageUrl', this should be considered required for feeds on-  -- the public web.-  , feedHomePageUrl :: Maybe Url-  -- ^ The URL of the resource that the feed describes. This resource may or-  -- may not actually be a "home" page, but it should be an HTML page. If a-  -- feed is published on the public web, this should be considered as-  -- required. But it may not make sense in the case of a file created on a-  -- desktop computer, when that file is not shared or is shared only-  -- privately.-  , feedHubs :: Maybe [Hub]-  -- ^ Describes endpoints that can be used to subscribe to real-time-  -- notifications from the publisher of this feed. Each object has a type and-  -- URL, both of which are required.-  , feedIcon :: Maybe Url-  -- ^ The URL of an image for the feed suitable to be used in a timeline, much-  -- the way an avatar might be used. It should be square and relatively large-  -- --- such as 512 x 512 --- so that it can be scaled-down and so that it can-  -- look good on retina displays. It should use transparency where-  -- appropriate, since it may be rendered on a non-white background.-  , feedItems :: [Item]-  -- ^ An array of objects that describe each object in the list.-  , feedNextUrl :: Maybe Url-  -- ^ The URL of a feed that provides the next /n/ items, where /n/ is-  -- determined by the publisher. This allows for pagination, but with the-  -- expectation that reader software is not required to use it and probably-  -- won't use it very often. 'feedNextUrl' must not be the same as-  -- 'feedFeedUrl', and it must not be the same as a previous 'feedNextUrl' (to-  -- avoid infinite loops).-  , feedTitle :: Text-  -- ^ The name of the feed, which will often correspond to the name of the-  -- website (blog, for instance), though not necessarily.-  , feedUserComment :: Maybe Text-  -- ^ A description of the purpose of the feed. This is for the use of people-  -- looking at the raw JSON, and should be ignored by feed readers.-  , feedVersion :: Url-  -- ^ The URL of the version of the format the feed uses.-  } deriving (Eq, Generic, Show)--instance FromJSON Feed where-  parseJSON = Json.genericParseJSON (jsonOptions "feed")--instance ToJSON Feed where-  toJSON = Json.genericToJSON (jsonOptions "feed")---data Author = Author-  { authorAvatar :: Maybe Url-  -- ^ The URL for an image for the author. As with icon, it should be square-  -- and relatively large --- such as 512 x 512 --- and should use transparency-  -- where appropriate, since it may be rendered on a non-white background.-  , authorName :: Maybe Text-  -- ^ The author's name.-  , authorUrl :: Maybe Url-  -- ^ The URL of a site owned by the author. It could be a blog, micro-blog,-  -- Twitter account, and so on. Ideally the linked-to page provides a way to-  -- contact the author, but that's not required. The URL could be a @mailto:@-  -- link, though we suspect that will be rare.-  } deriving (Eq, Generic, Show)--instance FromJSON Author where-  parseJSON value = do-    author <- Json.genericParseJSON (jsonOptions "author") value-    case (authorAvatar author, authorName author, authorUrl author) of-      (Nothing, Nothing, Nothing) -> fail ("invalid Author: " ++ show author)-      _ -> pure author--instance ToJSON Author where-  toJSON = Json.genericToJSON (jsonOptions "author")---data Item = Item-  { itemAttachments :: Maybe [Attachment]-  -- ^ Lists related resources. Podcasts, for instance, would include an-  -- attachment that's an audio or video file.-  , itemAuthor :: Maybe Author-  -- ^ Has the same structure as the top-level 'feedAuthor'. If not specified-  -- in an item, then the top-level author, if present, is the author of the-  -- item.-  , itemBannerImage :: Maybe Url-  -- ^ The URL of an image to use as a banner. Some blogging systems (such as-  -- Medium) display a different banner image chosen to go with each post, but-  -- that image wouldn't otherwise appear in the content_html. A feed reader-  -- with a detail view may choose to show this banner image at the top of the-  -- detail view, possibly with the title overlaid.-  , itemContentHtml :: Maybe Html-  -- ^ 'itemContentHtml' and 'itemContentText' are each optional strings ----  -- but one or both must be present. This is the HTML or plain text of the-  -- item. Important: the only place HTML is allowed in this format is in-  -- 'itemContentHtml'. A Twitter-like service might use 'itemContentText',-  -- while a blog might use 'itemContentHtml'. Use whichever makes sense for-  -- your resource. (It doesn't even have to be the same for each item in a-  -- feed.)-  , itemContentText :: Maybe Text-  -- ^ See 'itemContentHtml'.-  , itemDateModified :: Maybe UTCTime-  -- ^ Specifies the modification date in RFC 3339 format.-  , itemDatePublished :: Maybe UTCTime-  -- ^ Specifies the date in RFC 3339 format. (Example:-  -- @2010-02-07T14:04:00-05:00@.)-  , itemExternalUrl :: Maybe Url-  -- ^ The URL of a page elsewhere. This is especially useful for linkblogs. If-  -- 'itemUrl' links to where you're talking about a thing, then-  -- 'itemExternalUrl' links to the thing you're talking about.-  , itemId :: Value-  -- ^ Unique for the item in the feed over time. If an item is ever updated,-  -- the ID should be unchanged. New items should never use a previously-used-  -- ID. If an ID is presented as a number or other type, a JSON Feed reader-  -- must coerce it to a string. Ideally, the ID is the full URL of the-  -- resource described by the item, since URLs make great unique identifiers.-  , itemImage :: Maybe Url-  -- ^ The URL of the main image for the item. This image may also appear in-  -- the 'itemContentHtml' --- if so, it's a hint to the feed reader that this-  -- is the main, featured image. Feed readers may use the image as a preview-  -- (probably resized as a thumbnail and placed in a timeline).-  , itemSummary :: Maybe Text-  -- ^ A plain text sentence or two describing the item. This might be-  -- presented in a timeline, for instance, where a detail view would display-  -- all of 'itemContentHtml' or 'itemContentText'.-  , itemTags :: Maybe [Text]-  -- ^ Can have any plain text values you want. Tags tend to be just one word,-  -- but they may be anything. Note: they are not the equivalent of Twitter-  -- hashtags. Some blogging systems and other feed formats call these-  -- categories.-  , itemTitle :: Maybe Text-  -- ^ Plain text. Microblog items in particular may omit titles.-  , itemUrl :: Maybe Url-  -- ^ The URL of the resource described by the item. It's the permalink. This-  -- may be the same as the ID --- but should be present regardless.-  } deriving (Eq, Generic, Show)--instance FromJSON Item where-  parseJSON value = do-    item <- Json.genericParseJSON (jsonOptions "item") value-    case (itemContentHtml item, itemContentText item) of-      (Nothing, Nothing) -> fail ("invalid Item: " ++ show item)-      _ -> pure item--instance ToJSON Item where-  toJSON = Json.genericToJSON (jsonOptions "item")---data Attachment = Attachment-  { attachmentDurationInSeconds :: Maybe Natural-  -- ^ Specifies how long it takes to listen to or watch, when played at normal-  -- speed.-  , attachmentMimeType :: Mime-  -- ^ Specifies the type of the attachment, such as @audio/mpeg@.-  , attachmentSizeInBytes :: Maybe Natural-  -- ^ Specifies how large the file is.-  , attachmentTitle :: Maybe Text-  -- ^ Is a name for the attachment. Important: if there are multiple-  -- attachments, and two or more have the exact same title (when title is-  -- present), then they are considered as alternate representations of the-  -- same thing. In this way a podcaster, for instance, might provide an audio-  -- recording in different formats.-  , attachmentUrl :: Url-  -- ^ Specifies the location of the attachment.-  } deriving (Eq, Generic, Show)--instance FromJSON Attachment where-  parseJSON = Json.genericParseJSON (jsonOptions "attachment")--instance ToJSON Attachment where-  toJSON = Json.genericToJSON (jsonOptions "attachment")---data Hub = Hub-  { hubType :: Text-  , hubUrl :: Url-  } deriving (Eq, Generic, Show)--instance FromJSON Hub where-  parseJSON = Json.genericParseJSON (jsonOptions "hub")--instance ToJSON Hub where-  toJSON = Json.genericToJSON (jsonOptions "hub")---newtype Html = Html-  { htmlValue :: [Tag Text]-  } deriving (Eq, Show)--instance FromJSON Html where-  parseJSON = Json.withText "Html" (\text ->-    pure Html { htmlValue = Html.parseTags text })--instance ToJSON Html where-  toJSON html = Json.String (Html.renderTags (htmlValue html))---newtype Mime = Mime-  { mimeValue :: MimeType-  } deriving (Eq, Show)--instance FromJSON Mime where-  parseJSON = Json.withText "Mime" (\text ->-    pure Mime { mimeValue = Text.encodeUtf8 text })--instance ToJSON Mime where-  toJSON mime = Json.String (Text.decodeUtf8 (mimeValue mime))---newtype Url = Url-  { urlValue :: URI-  } deriving (Eq, Show)--instance FromJSON Url where-  parseJSON = Json.withText "Url" (\text ->-    case Uri.parseURI (Text.unpack text) of-      Just uri -> pure Url { urlValue = uri }-      Nothing -> fail ("invalid Url: " ++ show text))--instance ToJSON Url where-  toJSON url = Json.String (Text.pack (show (urlValue url)))---jsonOptions :: String -> Options-jsonOptions prefix =-  Json.defaultOptions-    { Json.fieldLabelModifier = fieldLabelModifier prefix-    }---fieldLabelModifier :: String -> String -> String-fieldLabelModifier prefix string =-  Json.camelTo2 '_' (unsafeDropPrefix prefix string)---unsafeDropPrefix :: String -> String -> String-unsafeDropPrefix prefix string =-  case dropPrefix prefix string of-    Just suffix -> suffix-    Nothing -> error (unwords-      [ "unsafeDropPrefix:"-      , show prefix-      , "is not a prefix of"-      , show string-      ])---dropPrefix :: String -> String -> Maybe String-dropPrefix prefix string =-  if List.isPrefixOf prefix string-    then Just (drop (length prefix) string)-    else Nothing
− package.yaml
@@ -1,53 +0,0 @@-name: json-feed-version: 1.0.9--category: Web-description: >-  This is an implementation of the JSON Feed spec in Haskell. The spec is-  available at <https://jsonfeed.org>. JSON Feed is similar to the Atom and RSS-  feed formats, but it is serialized as JSON rather than XML.-extra-source-files:-  - CHANGELOG.markdown-  - feeds/*.json-  - package.yaml-  - README.markdown-  - stack.yaml-github: tfausak/json-feed-license-file: LICENSE.markdown-license: MIT-maintainer: Taylor Fausak-synopsis: JSON Feed--dependencies:-  aeson: '>= 0.11.2 && < 0.12 || >= 1.0.2 && < 1.5'-  base: '>= 4.9.0 && < 4.15'-  bytestring: '>= 0.10.8 && < 0.11'-  mime-types: '>= 0.1.0 && < 0.2'-  network-uri: '>= 2.6.1 && < 2.8'-  tagsoup: '>= 0.14 && < 0.15'-  text: '>= 1.2.2 && < 1.3'-  time: '>= 1.6.0 && < 1.10'-ghc-options:-  - -Weverything-  - -Wno-implicit-prelude-  - -Wno-safe-  - -Wno-unsafe--when:-  - condition: impl(ghc >= 8.8.1)-    ghc-options: -Wno-missing-deriving-strategies--library:-  source-dirs: library--tests:-  test:-    dependencies:-      filepath: '>= 1.4.1 && < 1.5'-      hspec: '>= 2.2.3 && < 2.8'-      json-feed: -any-    ghc-options:-      - -rtsopts-      - -threaded-    main: Main.hs-    source-dirs: tests
+ src/lib/JsonFeed.hs view
@@ -0,0 +1,313 @@+{-# LANGUAGE DeriveGeneric #-}++-- | <https://jsonfeed.org>+module JsonFeed+  ( parseFeed+  , renderFeed+  -- * Types+  , Feed (..)+  , Author (..)+  , Item (..)+  , Attachment (..)+  , Hub (..)+  -- * Wrappers+  , Html (..)+  , Mime (..)+  , Url (..)+  ) where++import Data.Aeson (FromJSON, ToJSON, Value)+import Data.Aeson.Types (Options)+import Data.ByteString.Lazy (ByteString)+import Data.Text (Text)+import Data.Time (UTCTime)+import GHC.Generics (Generic)+import Network.Mime (MimeType)+import Network.URI (URI)+import Numeric.Natural (Natural)+import Text.HTML.TagSoup (Tag)++import qualified Data.Aeson as Json (eitherDecode, encode)+import qualified Data.Aeson.Types as Json+import qualified Data.List as List+import qualified Data.Text as Text+import qualified Data.Text.Encoding as Text+import qualified Network.URI as Uri+import qualified Text.HTML.TagSoup as Html+++parseFeed :: ByteString -> Either String Feed+parseFeed = Json.eitherDecode+++renderFeed :: Feed -> ByteString+renderFeed = Json.encode+++data Feed = Feed+  { feedAuthor :: Maybe Author+  -- ^ The feed author. The author object has several members. These are all+  -- optional --- but if you provide an author object, then at least one is+  -- required.+  , feedDescription :: Maybe Text+  -- ^ Provides more detail, beyond the title, on what the feed is about. A+  -- feed reader may display this text.+  , feedExpired :: Maybe Bool+  -- ^ Says whether or not the feed is finished --- that is, whether or not it+  -- will ever update again. A feed for a temporary event, such as an instance+  -- of the Olympics, could expire. If the value is 'True', then it's expired.+  -- Any other value, or the absence of 'feedExpired', means the feed may+  -- continue to update.+  , feedFavicon :: Maybe Url+  -- ^ The URL of an image for the feed suitable to be used in a source list.+  -- It should be square and relatively small, but not smaller than 64 x 64 (so+  -- that it can look good on retina displays). As with 'feedIcon', this image+  -- should use transparency where appropriate, since it may be rendered on a+  -- non-white background.+  , feedFeedUrl :: Maybe Url+  -- ^ The URL of the feed, and serves as the unique identifier for the feed.+  -- As with 'feedHomePageUrl', this should be considered required for feeds on+  -- the public web.+  , feedHomePageUrl :: Maybe Url+  -- ^ The URL of the resource that the feed describes. This resource may or+  -- may not actually be a "home" page, but it should be an HTML page. If a+  -- feed is published on the public web, this should be considered as+  -- required. But it may not make sense in the case of a file created on a+  -- desktop computer, when that file is not shared or is shared only+  -- privately.+  , feedHubs :: Maybe [Hub]+  -- ^ Describes endpoints that can be used to subscribe to real-time+  -- notifications from the publisher of this feed. Each object has a type and+  -- URL, both of which are required.+  , feedIcon :: Maybe Url+  -- ^ The URL of an image for the feed suitable to be used in a timeline, much+  -- the way an avatar might be used. It should be square and relatively large+  -- --- such as 512 x 512 --- so that it can be scaled-down and so that it can+  -- look good on retina displays. It should use transparency where+  -- appropriate, since it may be rendered on a non-white background.+  , feedItems :: [Item]+  -- ^ An array of objects that describe each object in the list.+  , feedNextUrl :: Maybe Url+  -- ^ The URL of a feed that provides the next /n/ items, where /n/ is+  -- determined by the publisher. This allows for pagination, but with the+  -- expectation that reader software is not required to use it and probably+  -- won't use it very often. 'feedNextUrl' must not be the same as+  -- 'feedFeedUrl', and it must not be the same as a previous 'feedNextUrl' (to+  -- avoid infinite loops).+  , feedTitle :: Text+  -- ^ The name of the feed, which will often correspond to the name of the+  -- website (blog, for instance), though not necessarily.+  , feedUserComment :: Maybe Text+  -- ^ A description of the purpose of the feed. This is for the use of people+  -- looking at the raw JSON, and should be ignored by feed readers.+  , feedVersion :: Url+  -- ^ The URL of the version of the format the feed uses.+  } deriving (Eq, Generic, Show)++instance FromJSON Feed where+  parseJSON = Json.genericParseJSON (jsonOptions "feed")++instance ToJSON Feed where+  toJSON = Json.genericToJSON (jsonOptions "feed")+++data Author = Author+  { authorAvatar :: Maybe Url+  -- ^ The URL for an image for the author. As with icon, it should be square+  -- and relatively large --- such as 512 x 512 --- and should use transparency+  -- where appropriate, since it may be rendered on a non-white background.+  , authorName :: Maybe Text+  -- ^ The author's name.+  , authorUrl :: Maybe Url+  -- ^ The URL of a site owned by the author. It could be a blog, micro-blog,+  -- Twitter account, and so on. Ideally the linked-to page provides a way to+  -- contact the author, but that's not required. The URL could be a @mailto:@+  -- link, though we suspect that will be rare.+  } deriving (Eq, Generic, Show)++instance FromJSON Author where+  parseJSON value = do+    author <- Json.genericParseJSON (jsonOptions "author") value+    case (authorAvatar author, authorName author, authorUrl author) of+      (Nothing, Nothing, Nothing) -> fail ("invalid Author: " ++ show author)+      _ -> pure author++instance ToJSON Author where+  toJSON = Json.genericToJSON (jsonOptions "author")+++data Item = Item+  { itemAttachments :: Maybe [Attachment]+  -- ^ Lists related resources. Podcasts, for instance, would include an+  -- attachment that's an audio or video file.+  , itemAuthor :: Maybe Author+  -- ^ Has the same structure as the top-level 'feedAuthor'. If not specified+  -- in an item, then the top-level author, if present, is the author of the+  -- item.+  , itemBannerImage :: Maybe Url+  -- ^ The URL of an image to use as a banner. Some blogging systems (such as+  -- Medium) display a different banner image chosen to go with each post, but+  -- that image wouldn't otherwise appear in the content_html. A feed reader+  -- with a detail view may choose to show this banner image at the top of the+  -- detail view, possibly with the title overlaid.+  , itemContentHtml :: Maybe Html+  -- ^ 'itemContentHtml' and 'itemContentText' are each optional strings ---+  -- but one or both must be present. This is the HTML or plain text of the+  -- item. Important: the only place HTML is allowed in this format is in+  -- 'itemContentHtml'. A Twitter-like service might use 'itemContentText',+  -- while a blog might use 'itemContentHtml'. Use whichever makes sense for+  -- your resource. (It doesn't even have to be the same for each item in a+  -- feed.)+  , itemContentText :: Maybe Text+  -- ^ See 'itemContentHtml'.+  , itemDateModified :: Maybe UTCTime+  -- ^ Specifies the modification date in RFC 3339 format.+  , itemDatePublished :: Maybe UTCTime+  -- ^ Specifies the date in RFC 3339 format. (Example:+  -- @2010-02-07T14:04:00-05:00@.)+  , itemExternalUrl :: Maybe Url+  -- ^ The URL of a page elsewhere. This is especially useful for linkblogs. If+  -- 'itemUrl' links to where you're talking about a thing, then+  -- 'itemExternalUrl' links to the thing you're talking about.+  , itemId :: Value+  -- ^ Unique for the item in the feed over time. If an item is ever updated,+  -- the ID should be unchanged. New items should never use a previously-used+  -- ID. If an ID is presented as a number or other type, a JSON Feed reader+  -- must coerce it to a string. Ideally, the ID is the full URL of the+  -- resource described by the item, since URLs make great unique identifiers.+  , itemImage :: Maybe Url+  -- ^ The URL of the main image for the item. This image may also appear in+  -- the 'itemContentHtml' --- if so, it's a hint to the feed reader that this+  -- is the main, featured image. Feed readers may use the image as a preview+  -- (probably resized as a thumbnail and placed in a timeline).+  , itemSummary :: Maybe Text+  -- ^ A plain text sentence or two describing the item. This might be+  -- presented in a timeline, for instance, where a detail view would display+  -- all of 'itemContentHtml' or 'itemContentText'.+  , itemTags :: Maybe [Text]+  -- ^ Can have any plain text values you want. Tags tend to be just one word,+  -- but they may be anything. Note: they are not the equivalent of Twitter+  -- hashtags. Some blogging systems and other feed formats call these+  -- categories.+  , itemTitle :: Maybe Text+  -- ^ Plain text. Microblog items in particular may omit titles.+  , itemUrl :: Maybe Url+  -- ^ The URL of the resource described by the item. It's the permalink. This+  -- may be the same as the ID --- but should be present regardless.+  } deriving (Eq, Generic, Show)++instance FromJSON Item where+  parseJSON value = do+    item <- Json.genericParseJSON (jsonOptions "item") value+    case (itemContentHtml item, itemContentText item) of+      (Nothing, Nothing) -> fail ("invalid Item: " ++ show item)+      _ -> pure item++instance ToJSON Item where+  toJSON = Json.genericToJSON (jsonOptions "item")+++data Attachment = Attachment+  { attachmentDurationInSeconds :: Maybe Natural+  -- ^ Specifies how long it takes to listen to or watch, when played at normal+  -- speed.+  , attachmentMimeType :: Mime+  -- ^ Specifies the type of the attachment, such as @audio/mpeg@.+  , attachmentSizeInBytes :: Maybe Natural+  -- ^ Specifies how large the file is.+  , attachmentTitle :: Maybe Text+  -- ^ Is a name for the attachment. Important: if there are multiple+  -- attachments, and two or more have the exact same title (when title is+  -- present), then they are considered as alternate representations of the+  -- same thing. In this way a podcaster, for instance, might provide an audio+  -- recording in different formats.+  , attachmentUrl :: Url+  -- ^ Specifies the location of the attachment.+  } deriving (Eq, Generic, Show)++instance FromJSON Attachment where+  parseJSON = Json.genericParseJSON (jsonOptions "attachment")++instance ToJSON Attachment where+  toJSON = Json.genericToJSON (jsonOptions "attachment")+++data Hub = Hub+  { hubType :: Text+  , hubUrl :: Url+  } deriving (Eq, Generic, Show)++instance FromJSON Hub where+  parseJSON = Json.genericParseJSON (jsonOptions "hub")++instance ToJSON Hub where+  toJSON = Json.genericToJSON (jsonOptions "hub")+++newtype Html = Html+  { htmlValue :: [Tag Text]+  } deriving (Eq, Show)++instance FromJSON Html where+  parseJSON = Json.withText "Html" (\text ->+    pure Html { htmlValue = Html.parseTags text })++instance ToJSON Html where+  toJSON html = Json.String (Html.renderTags (htmlValue html))+++newtype Mime = Mime+  { mimeValue :: MimeType+  } deriving (Eq, Show)++instance FromJSON Mime where+  parseJSON = Json.withText "Mime" (\text ->+    pure Mime { mimeValue = Text.encodeUtf8 text })++instance ToJSON Mime where+  toJSON mime = Json.String (Text.decodeUtf8 (mimeValue mime))+++newtype Url = Url+  { urlValue :: URI+  } deriving (Eq, Show)++instance FromJSON Url where+  parseJSON = Json.withText "Url" (\text ->+    case Uri.parseURI (Text.unpack text) of+      Just uri -> pure Url { urlValue = uri }+      Nothing -> fail ("invalid Url: " ++ show text))++instance ToJSON Url where+  toJSON url = Json.String (Text.pack (show (urlValue url)))+++jsonOptions :: String -> Options+jsonOptions prefix =+  Json.defaultOptions+    { Json.fieldLabelModifier = fieldLabelModifier prefix+    }+++fieldLabelModifier :: String -> String -> String+fieldLabelModifier prefix string =+  Json.camelTo2 '_' (unsafeDropPrefix prefix string)+++unsafeDropPrefix :: String -> String -> String+unsafeDropPrefix prefix string =+  case dropPrefix prefix string of+    Just suffix -> suffix+    Nothing -> error (unwords+      [ "unsafeDropPrefix:"+      , show prefix+      , "is not a prefix of"+      , show string+      ])+++dropPrefix :: String -> String -> Maybe String+dropPrefix prefix string =+  if List.isPrefixOf prefix string+    then Just (drop (length prefix) string)+    else Nothing
+ src/test/Main.hs view
@@ -0,0 +1,33 @@+module Main (main) where++import System.FilePath ((</>), (<.>))++import qualified Control.Monad as Monad+import qualified Data.ByteString.Lazy as ByteString+import qualified Data.Either as Either+import qualified JsonFeed+import qualified Test.Hspec as Hspec+++main :: IO ()+main = Hspec.hspec . Hspec.parallel . Hspec.describe "JsonFeed" $ do+  Monad.forM_ feeds $ \feed -> do+    Hspec.it ("parses the " ++ feed ++ " feed") $ do+      let file = "feeds" </> feed <.> "json"+      contents <- ByteString.readFile file+      JsonFeed.parseFeed contents `Hspec.shouldSatisfy` Either.isRight++feeds :: [String]+feeds =+  [ "allenpike.com"+  , "daringfireball.net"+  , "flyingmeat.com"+  , "hypercritical.co"+  , "inessential.com"+  , "jsonfeed.org"+  , "manton.org"+  , "maybepizza.com"+  , "shapeof.com"+  , "therecord.co"+  , "timetable.manton.org"+  ]
− stack.yaml
@@ -1,1 +0,0 @@-resolver: lts-15.4
− tests/Main.hs
@@ -1,33 +0,0 @@-module Main (main) where--import System.FilePath ((</>), (<.>))--import qualified Control.Monad as Monad-import qualified Data.ByteString.Lazy as ByteString-import qualified Data.Either as Either-import qualified JsonFeed-import qualified Test.Hspec as Hspec---main :: IO ()-main = Hspec.hspec . Hspec.parallel . Hspec.describe "JsonFeed" $ do-  Monad.forM_ feeds $ \feed -> do-    Hspec.it ("parses the " ++ feed ++ " feed") $ do-      let file = "feeds" </> feed <.> "json"-      contents <- ByteString.readFile file-      JsonFeed.parseFeed contents `Hspec.shouldSatisfy` Either.isRight--feeds :: [String]-feeds =-  [ "allenpike.com"-  , "daringfireball.net"-  , "flyingmeat.com"-  , "hypercritical.co"-  , "inessential.com"-  , "jsonfeed.org"-  , "manton.org"-  , "maybepizza.com"-  , "shapeof.com"-  , "therecord.co"-  , "timetable.manton.org"-  ]