├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── bower.json ├── shell.nix ├── src └── Record │ └── Format.purs └── test └── Main.purs /.gitignore: -------------------------------------------------------------------------------- 1 | /bower_components/ 2 | /node_modules/ 3 | /.pulp-cache/ 4 | /output/ 5 | /generated-docs/ 6 | /.psc* 7 | /.purs* 8 | /.psa* 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | dist: trusty 3 | sudo: required 4 | node_js: 8 5 | env: 6 | - PATH=$HOME/purescript:$PATH 7 | install: 8 | - TAG=$(wget -q -O - https://github.com/purescript/purescript/releases/latest --server-response --max-redirect 0 2>&1 | sed -n -e 's/.*Location:.*tag\///p') 9 | - wget -O $HOME/purescript.tar.gz https://github.com/purescript/purescript/releases/download/$TAG/linux64.tar.gz 10 | - tar -xvf $HOME/purescript.tar.gz -C $HOME/ 11 | - chmod a+x $HOME/purescript 12 | - npm install -g bower pulp 13 | script: 14 | - bower install --production 15 | - bower install 16 | - pulp test 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2018, Csongor Kiss 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # purescript-record-format 2 | 3 | Record formatting from type-level format strings, based on [Justin Woo](https://github.com/justinwoo)'s idea. 4 | 5 | This library uses the 0.12 version of the compiler. 6 | 7 | ## Example 8 | 9 | ```purescript 10 | format 11 | (SProxy :: SProxy "Hi {name}! Your favourite number is {number}") 12 | {name : "Bill", number : 16} 13 | ``` 14 | 15 | produces the string 16 | 17 | ``` 18 | "Hi Bill! Your favourite number is 16" 19 | ``` 20 | 21 | A missing field results in a type-error: 22 | 23 | ```purescript 24 | format 25 | (SProxy "Hi {name}! Your favourite number is {number}") 26 | {name : "Bill"} 27 | ``` 28 | 29 | ``` 30 | Could not match type 31 | 32 | ( number :: t2 33 | | t3 34 | ) 35 | 36 | with type 37 | 38 | ( name :: String 39 | ) 40 | ``` 41 | 42 | The only requirement is that all the types in the record have `Show` 43 | instances. 44 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "purescript-record-format", 3 | "ignore": [ 4 | "**/.*", 5 | "node_modules", 6 | "bower_components", 7 | "output" 8 | ], 9 | "license": "BSD-3-Clause", 10 | "repository": { 11 | "type": "git", 12 | "url": "git://github.com/kcsongor/purescript-record-format.git" 13 | }, 14 | "dependencies": { 15 | "purescript-strings": "^5.0.0", 16 | "purescript-record": "^3.0.0", 17 | "purescript-typelevel-prelude": "^6.0.0" 18 | }, 19 | "devDependencies": { 20 | "purescript-assert": "^5.0.0", 21 | "purescript-psci-support": "^5.0.0" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | { pkgs ? import { } }: 2 | let 3 | easy-ps = import 4 | (pkgs.fetchFromGitHub { 5 | owner = "justinwoo"; 6 | repo = "easy-purescript-nix"; 7 | rev = "5716cd791c999b3246b4fe173276b42c50afdd8d"; 8 | sha256 = "1r9lx4xhr42znmwb2x2pzah920klbjbjcivp2f0pnka7djvd2adq"; 9 | }) { 10 | inherit pkgs; 11 | }; 12 | in 13 | pkgs.mkShell { 14 | buildInputs = [ 15 | easy-ps.purs 16 | easy-ps.psc-package 17 | ]; 18 | } 19 | -------------------------------------------------------------------------------- /src/Record/Format.purs: -------------------------------------------------------------------------------- 1 | module Record.Format where 2 | 3 | import Prelude (identity, (<>), class Show, show) 4 | import Prim.Row as Row 5 | import Prim.Symbol as Symbol 6 | import Record as Record 7 | import Type.Data.Symbol (class IsSymbol, SProxy(..), reflectSymbol) 8 | 9 | -------------------------------------------------------------------------------- 10 | -- * Format strings 11 | 12 | data Fmt -- ^ a format token is... 13 | foreign import data Var :: Symbol -> Fmt -- ^ either a variable (to be replaced) 14 | foreign import data Lit :: Symbol -> Fmt -- ^ or a literal 15 | 16 | -- | A list of format tokens 17 | data FList 18 | foreign import data FNil :: FList 19 | foreign import data FCons :: Fmt -> FList -> FList 20 | 21 | data FProxy (fl :: FList) = FProxy 22 | 23 | -- | Format a row with a (type-level) format string. If @row@ doesn't contain 24 | -- all the necessary fields, constraint resolution fails 25 | class Format (string :: Symbol) (row :: Row Type) where 26 | format :: SProxy string -> Record row -> String 27 | 28 | -- parse the format string and delegate the formatting to @FormatParsed@ 29 | instance formatParsedFormat :: 30 | ( Parse string parsed 31 | , FormatParsed parsed row 32 | ) => Format string row where 33 | format _ = formatParsed (FProxy :: FProxy parsed) 34 | 35 | -- | Format a row with a list of format tokens. If @row@ doesn't contain 36 | -- all the necessary fields, constraint resolution fails 37 | class FormatParsed (strings :: FList) (row :: Row Type) where 38 | formatParsed :: FProxy strings -> Record row -> String 39 | 40 | instance formatFNil :: FormatParsed FNil row where 41 | formatParsed _ _ = "" 42 | 43 | instance formatVar :: 44 | ( IsSymbol key 45 | , Row.Cons key typ tail row 46 | , FormatParsed ks row 47 | , FormatVar typ 48 | ) => FormatParsed (FCons (Var key) ks) row where 49 | formatParsed _ row 50 | = var <> rest 51 | where var = fmtVar (Record.get (SProxy :: SProxy key) row) 52 | rest = formatParsed (FProxy :: FProxy ks) row 53 | 54 | instance formatLit :: 55 | ( IsSymbol l 56 | , FormatParsed ks row 57 | ) => FormatParsed (FCons (Lit l) ks) row where 58 | formatParsed _ row 59 | = lit <> rest 60 | where lit = reflectSymbol (SProxy :: SProxy l) 61 | rest = formatParsed (FProxy :: FProxy ks) row 62 | 63 | -- | Formatting variables - we don't want to show the quotes around strings, so 64 | -- we treat them specially 65 | class FormatVar a where 66 | fmtVar :: a -> String 67 | 68 | instance aFmtVar :: FormatVar String where 69 | fmtVar = identity 70 | else instance bFmtVar :: Show a => FormatVar a where 71 | fmtVar = show 72 | 73 | -------------------------------------------------------------------------------- 74 | -- Parsing 75 | 76 | class Parse (i :: Symbol) (o :: FList) | i -> o 77 | 78 | instance aParse :: Parse "" FNil 79 | else instance bParse :: (Symbol.Cons h t i, ParseLit h t o) => Parse i o 80 | 81 | -- | Parse literals. @h@ is the current character, @t@ is the remaining string 82 | class ParseLit (h :: Symbol) (t :: Symbol) (o :: FList) | h t -> o 83 | 84 | instance aParseLitNil :: ParseLit o "" (FCons (Lit o) FNil) 85 | -- when we find a '{' character, call @ParseVar@ 86 | else instance bParseLitVar :: 87 | ( Symbol.Cons h' t' t 88 | , ParseVar h' t' (Var match) rest 89 | , Parse rest pRest 90 | ) => ParseLit "{" t (FCons (Lit "") (FCons (Var match) pRest)) 91 | else instance cParseLit :: 92 | ( Parse i (FCons (Lit l) fs) 93 | , Symbol.Cons c l cl 94 | ) => ParseLit c i (FCons (Lit cl) fs) 95 | 96 | -- | Parse variables. Returns the symbol between {}s and the remaining string 97 | -- after the closing '}' 98 | class ParseVar (h :: Symbol) (t :: Symbol) (var :: Fmt) (rest :: Symbol) | h t -> var rest 99 | 100 | instance aParseVar :: ParseVar "" a (Var "") "" 101 | else instance bParseVar :: ParseVar "}" i (Var "") i 102 | else instance cParseVar :: ParseVar curr "" (Var curr) "" 103 | else instance dParseVar :: 104 | ( Symbol.Cons h' t' t 105 | , ParseVar h' t' (Var var) rest 106 | , Symbol.Cons h var var' 107 | ) => ParseVar h t (Var var') rest 108 | 109 | parse :: forall i o. Parse i o => SProxy i -> FProxy o 110 | parse _ = FProxy :: FProxy o 111 | -------------------------------------------------------------------------------- /test/Main.purs: -------------------------------------------------------------------------------- 1 | module Test.Main where 2 | 3 | import Prelude 4 | 5 | import Effect (Effect) 6 | import Record.Format (format) 7 | import Test.Assert (assert) 8 | import Type.Prelude (SProxy(..)) 9 | 10 | main :: Effect Unit 11 | main = do 12 | let formatted = format (SProxy :: SProxy "Hi {name}! You are {number}") {name : "Bill", number : 16} 13 | assert $ formatted == "Hi Bill! You are 16" 14 | --------------------------------------------------------------------------------