realdentalcosts (empty) → 0.1.0.0
raw patch · 4 files changed
+211/−0 lines, 4 filesdep +aesondep +basedep +http-conduit
Dependencies added: aeson, base, http-conduit, text
Files
- LICENSE +21/−0
- README.md +25/−0
- realdentalcosts.cabal +33/−0
- src/RealDentalCosts/Api.hs +132/−0
+ LICENSE view
@@ -0,0 +1,21 @@+MIT License++Copyright (c) 2026 Real Dental Costs Data & Research Team++Permission is hereby granted, free of charge, to any person obtaining a copy+of this software and associated documentation files (the "Software"), to deal+in the Software without restriction, including without limitation the rights+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell+copies of the Software, and to permit persons to whom the Software is+furnished to do so, subject to the following conditions:++The above copyright notice and this permission notice shall be included in all+copies or substantial portions of the Software.++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE+SOFTWARE.
+ README.md view
@@ -0,0 +1,25 @@+# realdentalcosts (Haskell)++Typed client for the [Real Dental Costs Open Data API](https://realdentalcosts.com/en/api/):+US dental procedure cash price ranges by state, state cost indexes and Medicaid paid amounts+per CDT code (HHS/CMS T-MSIS, 2018-2024). Data CC BY 4.0, code MIT.++```haskell+import RealDentalCosts.Api++main :: IO ()+main = do+ Right p <- getProcedure "root-canal"+ print (avgUsd (procNational p), stateRow "TX" p, provConfidence (procProvenance p))+ Right m <- getMedicaid "D3330"+ print (msPaidPerLine (mdNational m))+```++Human-readable tables: [root canal cost by state](https://realdentalcosts.com/en/root-canal/),+[denture cost by state](https://realdentalcosts.com/en/dentures/),+[dental implant cost by state](https://realdentalcosts.com/en/dental-implants/),+[Medicaid dental coverage by state](https://realdentalcosts.com/en/medicaid-dental-coverage-by-state/).+Methodology: <https://realdentalcosts.com/en/methodology/>. Dataset DOI 10.7910/DVN/7F2BZI.+Pricing and market research, not medical advice.++Status: source published; Hackage upload pending uploader-group approval.
+ realdentalcosts.cabal view
@@ -0,0 +1,33 @@+cabal-version: 2.4+name: realdentalcosts+version: 0.1.0.0+synopsis: Client for the Real Dental Costs Open Data API (US dental prices)+description:+ Typed client for the Real Dental Costs Open Data API+ (<https://realdentalcosts.com/en/api/>): US dental procedure cash price+ ranges by state, state cost indexes and Medicaid paid amounts per CDT code+ derived from the HHS/CMS T-MSIS provider spending files. Every payload+ carries a provenance block (basis, confidence, sample size). Data licence+ CC BY 4.0; see <https://realdentalcosts.com/en/methodology/>.+homepage: https://realdentalcosts.com/en/api/+bug-reports: https://codeberg.org/tresor2k/realdentalcosts-hs/issues+license: MIT+license-file: LICENSE+author: Real Dental Costs Data and Research Team+maintainer: research@realdentalcosts.com+category: Web, Data+build-type: Simple+extra-doc-files: README.md++source-repository head+ type: git+ location: https://codeberg.org/tresor2k/realdentalcosts-hs.git++library+ exposed-modules: RealDentalCosts.Api+ build-depends: base >=4.14 && <5,+ aeson >=2.0 && <2.3,+ http-conduit >=2.3 && <2.4,+ text >=1.2 && <2.2+ hs-source-dirs: src+ default-language: Haskell2010
+ src/RealDentalCosts/Api.hs view
@@ -0,0 +1,132 @@+{-# LANGUAGE OverloadedStrings #-}+-- | Client for the Real Dental Costs Open Data API+-- (<https://realdentalcosts.com/en/api/>).+--+-- The API is static JSON (CC BY 4.0) covering US dental procedure cash price+-- ranges by state (<https://realdentalcosts.com/en/root-canal/>,+-- <https://realdentalcosts.com/en/dentures/>,+-- <https://realdentalcosts.com/en/dental-implants/>), state cost indexes and+-- Medicaid paid amounts per CDT code from the HHS/CMS T-MSIS files+-- (<https://realdentalcosts.com/en/medicaid-dental-coverage-by-state/>).+-- Every payload carries a @provenance@ block; see+-- <https://realdentalcosts.com/en/methodology/>.+module RealDentalCosts.Api+ ( baseUrl+ , Provenance (..)+ , PriceRange (..)+ , StatePrice (..)+ , Procedure (..)+ , MedicaidStats (..)+ , MedicaidState (..)+ , Medicaid (..)+ , getProcedure+ , getState+ , getMedicaid+ , getJSON+ , stateRow+ ) where++import Data.Aeson (FromJSON (..), Value, withObject, (.:), (.:?), eitherDecode)+import Data.Char (toLower, toUpper)+import Data.List (find)+import Data.Text (Text)+import Network.HTTP.Simple (getResponseBody, getResponseStatusCode, httpLBS, parseRequest, setRequestHeader)++-- | Production API root.+baseUrl :: String+baseUrl = "https://realdentalcosts.com/api/v1"++-- | Where a figure comes from and how much to trust it.+data Provenance = Provenance+ { provBasis :: Text -- ^ "observed" or "estimated"+ , provModeled :: Bool+ , provConfidence :: Int -- ^ 0-100+ , provSampleN :: Maybe Int+ , provPeriod :: Maybe Text+ } deriving (Show, Eq)++instance FromJSON Provenance where+ parseJSON = withObject "Provenance" $ \o ->+ Provenance <$> o .: "basis" <*> o .: "modeled" <*> o .: "confidence"+ <*> o .:? "sample_n" <*> o .:? "period"++data PriceRange = PriceRange { lowUsd :: Int, avgUsd :: Int, highUsd :: Int }+ deriving (Show, Eq)++instance FromJSON PriceRange where+ parseJSON = withObject "PriceRange" $ \o ->+ PriceRange <$> o .: "low_usd" <*> o .: "avg_usd" <*> o .: "high_usd"++data StatePrice = StatePrice+ { spState :: Text, spCode :: Text, spLow :: Int, spAvg :: Int, spHigh :: Int, spConfidence :: Int }+ deriving (Show, Eq)++instance FromJSON StatePrice where+ parseJSON = withObject "StatePrice" $ \o ->+ StatePrice <$> o .: "state" <*> o .: "code" <*> o .: "low_usd" <*> o .: "avg_usd"+ <*> o .: "high_usd" <*> o .: "confidence"++-- | Payload of @/procedure/{slug}.json@.+data Procedure = Procedure+ { procSlug :: Text, procLabel :: Text, procBasis :: Text+ , procProvenance :: Provenance, procNational :: PriceRange, procByState :: [StatePrice] }+ deriving (Show, Eq)++instance FromJSON Procedure where+ parseJSON = withObject "Procedure" $ \o ->+ Procedure <$> o .: "slug" <*> o .: "label" <*> o .: "basis" <*> o .: "provenance"+ <*> o .: "national" <*> o .: "by_state"++data MedicaidStats = MedicaidStats+ { msPaidPerLine :: Double, msMedianCell :: Double, msLines :: Int, msPatients :: Int, msProviders :: Int }+ deriving (Show, Eq)++instance FromJSON MedicaidStats where+ parseJSON = withObject "MedicaidStats" $ \o ->+ MedicaidStats <$> o .: "paid_per_line_wavg" <*> o .: "median_cell" <*> o .: "lines"+ <*> o .: "patients" <*> o .: "n_providers"++data MedicaidState = MedicaidState+ { mstState :: Text, mstCode :: Text, mstQuality :: Text, mstConfidence :: Int, mstLatest :: Maybe MedicaidStats }+ deriving (Show, Eq)++instance FromJSON MedicaidState where+ parseJSON = withObject "MedicaidState" $ \o ->+ MedicaidState <$> o .: "state" <*> o .: "code" <*> o .: "quality_flag" <*> o .: "confidence" <*> o .:? "latest"++-- | Payload of @/medicaid/{cdt}.json@ (e.g. @D3330@ molar root canal).+data Medicaid = Medicaid+ { mdCode :: Text, mdLabel :: Text, mdProvenance :: Provenance, mdNational :: MedicaidStats+ , mdStates :: [MedicaidState], mdCaveats :: [Text] }+ deriving (Show, Eq)++instance FromJSON Medicaid where+ parseJSON = withObject "Medicaid" $ \o ->+ Medicaid <$> o .: "code" <*> o .: "label" <*> o .: "provenance" <*> o .: "national"+ <*> o .: "states" <*> o .: "caveats"++-- | Fetch and decode any endpoint path (e.g. @"/states.json"@).+getJSON :: FromJSON a => String -> IO (Either String a)+getJSON path = do+ req <- setRequestHeader "User-Agent" ["realdentalcosts-hs/0.1"] <$> parseRequest (baseUrl ++ path)+ resp <- httpLBS req+ let code = getResponseStatusCode resp+ pure $ if code == 404 then Left ("not found: " ++ path)+ else if code < 200 || code > 299 then Left ("HTTP " ++ show code ++ ": " ++ path)+ else eitherDecode (getResponseBody resp)++-- | One procedure by slug (e.g. @"root-canal"@, @"dentures"@, @"dental-implant"@).+getProcedure :: String -> IO (Either String Procedure)+getProcedure slug = getJSON ("/procedure/" ++ map toLower slug ++ ".json")++-- | One state by two-letter code; returned as a raw JSON 'Value'.+getState :: String -> IO (Either String Value)+getState code = getJSON ("/state/" ++ map toLower code ++ ".json")++-- | Medicaid paid amounts per claim line for a CDT code.+getMedicaid :: String -> IO (Either String Medicaid)+getMedicaid cdt = getJSON ("/medicaid/" ++ map toUpper cdt ++ ".json")++-- | Find a state's row in a procedure table.+stateRow :: Text -> Procedure -> Maybe StatePrice+stateRow code = find ((== code) . spCode) . procByState