diff --git a/ChangeLog.md b/ChangeLog.md
new file mode 100644
--- /dev/null
+++ b/ChangeLog.md
@@ -0,0 +1,5 @@
+# Changelog for attoparsec-fakedata
+
+## 0.1.0
+
+* Initial release.
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,30 @@
+Copyright Sibi Prabakaran (c) 2020
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+
+    * Neither the name of Sibi Prabakaran nor the names of other
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,3 @@
+# fakedata-parser
+
+Parser used by the package for resolving fake texts.
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/fakedata-parser.cabal b/fakedata-parser.cabal
new file mode 100644
--- /dev/null
+++ b/fakedata-parser.cabal
@@ -0,0 +1,55 @@
+cabal-version: 1.12
+
+-- This file has been generated from package.yaml by hpack version 0.33.0.
+--
+-- see: https://github.com/sol/hpack
+--
+-- hash: 3bf71bf61a215c60ae1371eaa4488fd6b76805d327042c466c352a013f873364
+
+name:           fakedata-parser
+version:        0.1.0.0
+description:    Please see the README on GitHub at <https://github.com/psibi/fakedata-parser#readme>
+homepage:       https://github.com/psibi/fakedata-parser#readme
+bug-reports:    https://github.com/psibi/fakedata-parser/issues
+author:         Sibi Prabakaran
+maintainer:     sibi@psibi.in
+copyright:      Sibi Prabakaran
+license:        BSD3
+license-file:   LICENSE
+build-type:     Simple
+extra-source-files:
+    README.md
+    ChangeLog.md
+
+source-repository head
+  type: git
+  location: https://github.com/psibi/fakedata-parser
+
+library
+  exposed-modules:
+      Fakedata.Parser
+  other-modules:
+      Paths_fakedata_parser
+  hs-source-dirs:
+      src
+  build-depends:
+      attoparsec
+    , base >=4.7 && <5
+    , text
+  default-language: Haskell2010
+
+test-suite fakedata-parser-test
+  type: exitcode-stdio-1.0
+  main-is: Spec.hs
+  other-modules:
+      Paths_fakedata_parser
+  hs-source-dirs:
+      test
+  ghc-options: -threaded -rtsopts -with-rtsopts=-N
+  build-depends:
+      attoparsec
+    , base >=4.7 && <5
+    , fakedata-parser
+    , hspec
+    , text
+  default-language: Haskell2010
diff --git a/src/Fakedata/Parser.hs b/src/Fakedata/Parser.hs
new file mode 100644
--- /dev/null
+++ b/src/Fakedata/Parser.hs
@@ -0,0 +1,65 @@
+module Fakedata.Parser
+  ( FakeIRValue(..)
+  , parseLiteralText
+  , parseHash
+  , parseFakedata
+  ) where
+
+import Control.Applicative
+import Control.Monad (void)
+import qualified Data.Attoparsec.Text as P
+import Data.Text (Text)
+import qualified Data.Text as T
+
+-- | Type representing the unresolved text
+data FakeIRValue
+  = Literal Text
+  | Hash Int
+  | Ques Int
+  | Resolve Text
+  deriving (Show, Eq)
+
+parseLiteralText :: P.Parser FakeIRValue
+parseLiteralText = do
+  literal <- some $ P.satisfy (not . isHashOrQues)
+  pure $ Literal $ T.pack literal
+
+parseHash :: P.Parser [FakeIRValue]
+parseHash = do
+  hashes <- some $ P.char '#'
+  let numHahes = Prelude.length hashes
+  nh <- P.peekChar
+  case nh of
+    Nothing -> pure $ [Hash numHahes]
+    Just c ->
+      if c == '{'
+        then do
+          void P.anyChar
+          xs <- P.takeTill (\a -> a == '}')
+          void P.anyChar
+          case numHahes of
+            0 -> fail "parseHash: undefined state"
+            1 -> pure $ [Resolve xs]
+            n -> pure $ [Hash (numHahes - 1), Resolve xs]
+        else pure $ [Hash numHahes]
+
+parseQues :: P.Parser FakeIRValue
+parseQues = do
+  ques <- some $ P.char '?'
+  pure $ Ques (Prelude.length ques)
+
+singleton :: a -> [a]
+singleton x = [x]
+
+parseFakedata :: P.Parser [FakeIRValue]
+parseFakedata = do
+  xs <-
+    many $
+    ((singleton <$> parseLiteralText) <|> parseHash <|>
+     (singleton <$> parseQues))
+  pure $ concat xs
+
+isHashOrQues :: Char -> Bool
+isHashOrQues '?' = True
+isHashOrQues '#' = True
+isHashOrQues _ = False
diff --git a/test/Spec.hs b/test/Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Spec.hs
@@ -0,0 +1,88 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+import Control.Applicative
+import Data.Attoparsec.Text as P
+import Data.Text
+import Fakedata.Parser
+import Test.Hspec
+
+main :: IO ()
+main =
+  hspec $ do
+    describe "parseLiteral" $ do
+      it "parses resolved text" $ do
+        let result = P.parseOnly parseLiteralText "hello world"
+        result `shouldBe` (Right $ Literal "hello world")
+      it "parses resolved text and hash" $ do
+        let result = P.parseOnly parseLiteralText "hello world#"
+        result `shouldBe` (Right $ Literal "hello world")
+      it "parses resolved text and ?" $ do
+        let result = P.parseOnly parseLiteralText "hello wo?rld?#"
+        result `shouldBe` (Right $ Literal "hello wo")
+    describe "parseHash" $ do
+      it "works with - #####" $ do
+        let result = P.parseOnly parseHash "#####"
+        result `shouldBe` (Right [Hash 5])
+      it "works with - ###{hello}" $ do
+        let result = P.parseOnly parseHash "###{hello}"
+        result `shouldBe` (Right [Hash 2, Resolve "hello"])
+      it "works with - #{hello}" $ do
+        let result = P.parseOnly parseHash "#{hello}"
+        result `shouldBe` (Right [Resolve "hello"])
+    describe "parseFakedata" $ do
+      it "works with - hello world" $ do
+        let result = P.parseOnly parseFakedata "hello world"
+        result `shouldBe` (Right $ (Literal "hello world") : [])
+      it "works with - hello world ###" $ do
+        let result = P.parseOnly parseFakedata "hello world ###"
+        result `shouldBe` (Right $ (Literal "hello world ") : [Hash 3])
+      it "works with - hello world ### ??" $ do
+        let result = P.parseOnly parseFakedata "hello world ### ??"
+        result `shouldBe`
+          (Right $ (Literal "hello world ") : [Hash 3, Literal " ", Ques 2])
+      it "works with - ###" $ do
+        let result = P.parseOnly parseFakedata "###"
+        result `shouldBe` (Right $ (Hash 3) : [])
+      it "works with - ???" $ do
+        let result = P.parseOnly parseFakedata "???"
+        result `shouldBe` (Right $ (Ques 3) : [])
+      it "works with - ???hello" $ do
+        let result = P.parseOnly parseFakedata "???hello"
+        result `shouldBe` (Right $ (Ques 3) : [Literal "hello"])
+      it "works with - ???hello #{hello} hi #{world}" $ do
+        let result = P.parseOnly parseFakedata "???hello #{hello} hi #{world}"
+        result `shouldBe`
+          (Right $
+           (Ques 3) :
+           [Literal "hello ", Resolve "hello", Literal " hi ", Resolve "world"])
+    describe "unresolved text in address" $ do
+      it "works with - PO Box ####" $ do
+        let result = P.parseOnly parseFakedata "PO Box ####"
+        result `shouldBe` (Right $ (Literal "PO Box ") : [Hash 4])
+      it
+        "works with - #{secondary_address} #{street_address}, #{city}, #{state_abbr} #{zip_code}" $ do
+        let result =
+              P.parseOnly
+                parseFakedata
+                "#{secondary_address} #{street_address}, #{city}, #{state_abbr} #{zip_code}"
+        result `shouldBe`
+          (Right $
+           (Resolve "secondary_address") :
+           [ Literal " "
+           , Resolve "street_address"
+           , Literal ", "
+           , Resolve "city"
+           , Literal ", "
+           , Resolve "state_abbr"
+           , Literal " "
+           , Resolve "zip_code"
+           ])
+      it "works with - #{Name.last_name}#{city_suffix}" $ do
+        let result = P.parseOnly parseFakedata "#{Name.last_name}#{city_suffix}"
+        result `shouldBe` (Right $ (Resolve "Name.last_name") : [Resolve "city_suffix"])
+      it "works with - 831##" $ do
+        let result = P.parseOnly parseFakedata "831##"
+        result `shouldBe` (Right $ (Literal "831") : [Hash 2])
+      it "works with - #####-####" $ do
+        let result = P.parseOnly parseFakedata "#####-####"
+        result `shouldBe` (Right $ (Hash 5) : [Literal "-", Hash 4])
