├── .gitignore ├── .travis.yml ├── LICENSE ├── Makefile ├── README.md ├── Setup.hs ├── Vagrantfile ├── bench └── HCLBenchmark.hs ├── benchmark.html ├── circle.yml ├── data ├── array_comment.hcl ├── array_comment_2.hcl ├── assign_colon.hcl ├── assign_deep.hcl ├── comment.hcl ├── comment_lastline.hcl ├── comment_single.hcl ├── complex.hcl ├── complex_key.hcl ├── empty.hcl ├── key_without_value.hcl ├── list.hcl ├── list_comma.hcl ├── missing_braces.hcl ├── multiple.hcl ├── object_key_without_value.hcl ├── old.hcl ├── structure.hcl ├── structure_basic.hcl ├── structure_empty.hcl ├── types.hcl ├── unterminated_object.hcl └── unterminated_object_2.hcl ├── default.conf ├── package.yaml ├── small.conf ├── src └── Data │ ├── HCL.hs │ └── HCL │ └── Types.hs ├── stack.yaml ├── test-fixtures ├── basic.hcl ├── basic.json ├── basic_int_string.hcl ├── basic_squish.hcl ├── decode_policy.hcl ├── decode_policy.json ├── decode_tf_variable.hcl ├── decode_tf_variable.json ├── empty.hcl ├── escape.hcl ├── flat.hcl ├── float.hcl ├── float.json ├── interpolate_escape.hcl ├── multiline.hcl ├── multiline.json ├── multiline_bad.hcl ├── multiline_indented.hcl ├── multiline_no_eof.hcl ├── multiline_no_hanging_indent.hcl ├── multiline_no_marker.hcl ├── nested_block_comment.hcl ├── nested_provider_bad.hcl ├── object_list.json ├── scientific.hcl ├── scientific.json ├── slice_expand.hcl ├── structure.hcl ├── structure.json ├── structure2.hcl ├── structure2.json ├── structure_flat.json ├── structure_flatmap.hcl ├── structure_list.hcl ├── structure_list.json ├── structure_list_deep.json ├── structure_multi.hcl ├── structure_multi.json ├── terraform_heroku.hcl ├── terraform_heroku.json ├── tfvars.hcl ├── unterminated_block_comment.hcl └── unterminated_brace.hcl └── test ├── Data ├── HCL │ ├── PrettyPrintSpec.hs │ └── TestHelper.hs └── HCLSpec.hs ├── SanitySpec.hs └── Spec.hs /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/haskell 3 | 4 | ### Haskell ### 5 | dist 6 | dist-* 7 | cabal-dev 8 | *.o 9 | *.hi 10 | *.chi 11 | *.chs.h 12 | *.dyn_o 13 | *.dyn_hi 14 | .hpc 15 | .hsenv 16 | .cabal-sandbox/ 17 | cabal.sandbox.config 18 | *.prof 19 | *.aux 20 | *.hp 21 | *.eventlog 22 | .stack-work/ 23 | cabal.project.local 24 | .vagrant 25 | *.cabal 26 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # This is the complex Travis configuration, which is intended for use 2 | # on open source libraries which need compatibility across multiple GHC 3 | # versions, must work with cabal-install, and should be 4 | # cross-platform. For more information and other options, see: 5 | # 6 | # https://docs.haskellstack.org/en/stable/travis_ci/ 7 | # 8 | # Copy these contents into the root directory of your Github project in a file 9 | # named .travis.yml 10 | 11 | # Use new container infrastructure to enable caching 12 | sudo: false 13 | 14 | # Do not choose a language; we provide our own build tools. 15 | language: generic 16 | 17 | # Caching so the next build will be fast too. 18 | cache: 19 | directories: 20 | - $HOME/.ghc 21 | - $HOME/.cabal 22 | - $HOME/.stack 23 | 24 | # The different configurations we want to test. We have BUILD=cabal which uses 25 | # cabal-install, and BUILD=stack which uses Stack. More documentation on each 26 | # of those below. 27 | # 28 | # We set the compiler values here to tell Travis to use a different 29 | # cache file per set of arguments. 30 | # 31 | # If you need to have different apt packages for each combination in the 32 | # matrix, you can use a line such as: 33 | # addons: {apt: {packages: [libfcgi-dev,libgmp-dev]}} 34 | matrix: 35 | include: 36 | - env: BUILD=stack 37 | compiler: ": #stack 8.0.2" 38 | addons: {apt: {packages: [libgmp-dev]}} 39 | 40 | allow_failures: 41 | - env: BUILD=cabal GHCVER=head CABALVER=head HAPPYVER=1.19.5 ALEXVER=3.1.7 42 | - env: BUILD=stack ARGS="--resolver nightly" 43 | 44 | # Using compiler above sets CC to an invalid value, so unset it 45 | - unset CC 46 | 47 | # We want to always allow newer versions of packages when building on GHC HEAD 48 | - CABALARGS="" 49 | - if [ "x$GHCVER" = "xhead" ]; then CABALARGS=--allow-newer; fi 50 | 51 | # Download and unpack the stack executable 52 | - export PATH=/opt/ghc/$GHCVER/bin:/opt/cabal/$CABALVER/bin:$HOME/.local/bin:/opt/alex/$ALEXVER/bin:/opt/happy/$HAPPYVER/bin:$HOME/.cabal/bin:$PATH 53 | - mkdir -p ~/.local/bin 54 | - | 55 | if [ `uname` = "Darwin" ] 56 | then 57 | brew install icu4c 58 | travis_retry curl --insecure -L https://www.stackage.org/stack/osx-x86_64 | tar xz --strip-components=1 --include '*/stack' -C ~/.local/bin 59 | else 60 | travis_retry curl -L https://www.stackage.org/stack/linux-x86_64 | tar xz --wildcards --strip-components=1 -C ~/.local/bin '*/stack' 61 | fi 62 | 63 | # Use the more reliable S3 mirror of Hackage 64 | mkdir -p $HOME/.cabal 65 | echo 'remote-repo: hackage.haskell.org:http://hackage.fpcomplete.com/' > $HOME/.cabal/config 66 | echo 'remote-repo-cache: $HOME/.cabal/packages' >> $HOME/.cabal/config 67 | 68 | if [ "$CABALVER" != "1.16" ] 69 | then 70 | echo 'jobs: $ncpus' >> $HOME/.cabal/config 71 | fi 72 | 73 | install: 74 | - git submodule update --init --recursive 75 | - echo "$(ghc --version) [$(ghc --print-project-git-commit-id 2> /dev/null || echo '?')]" 76 | - if [ -f configure.ac ]; then autoreconf -i; fi 77 | - | 78 | set -ex 79 | case "$BUILD" in 80 | stack) 81 | stack --no-terminal --install-ghc $ARGS test --bench --only-dependencies 82 | ;; 83 | cabal) 84 | cabal --version 85 | travis_retry cabal update 86 | 87 | # Get the list of packages from the stack.yaml file 88 | PACKAGES=$(stack --install-ghc query locals | grep '^ *path' | sed 's@^ *path:@@') 89 | 90 | cabal install --only-dependencies --enable-tests --enable-benchmarks --force-reinstalls --ghc-options=-O0 --reorder-goals --max-backjumps=-1 $CABALARGS $PACKAGES 91 | ;; 92 | esac 93 | set +ex 94 | 95 | script: 96 | - | 97 | set -ex 98 | case "$BUILD" in 99 | stack) 100 | stack --no-terminal $ARGS test --bench --no-run-benchmarks --haddock --no-haddock-deps 101 | ;; 102 | cabal) 103 | cabal install --enable-tests --enable-benchmarks --force-reinstalls --ghc-options=-O0 --reorder-goals --max-backjumps=-1 $CABALARGS $PACKAGES 104 | 105 | ORIGDIR=$(pwd) 106 | for dir in $PACKAGES 107 | do 108 | cd $dir 109 | cabal check || [ "$CABALVER" == "1.16" ] 110 | cabal sdist 111 | PKGVER=$(cabal info . | awk '{print $2;exit}') 112 | SRC_TGZ=$PKGVER.tar.gz 113 | cd dist 114 | tar zxfv "$SRC_TGZ" 115 | cd "$PKGVER" 116 | cabal configure --enable-tests 117 | cabal build 118 | cd $ORIGDIR 119 | done 120 | ;; 121 | esac 122 | set +ex 123 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Pedro Tacla Yamada 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: FORCE 2 | stack setup 3 | stack build 4 | stack test 5 | 6 | build: FORCE 7 | stack build 8 | 9 | test: FORCE 10 | stack test 11 | 12 | FORCE: 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # haskell-language-hcl 2 | [![Hackage](https://img.shields.io/hackage/v/language-hcl.svg?maxAge=2592000)](https://hackage.haskell.org/package/language-hcl) 3 | - - - 4 | **`language-hcl`** contains HCL (Hashicorp Configuration Language) parsers and 5 | pretty-printers for the Haskell programming language. 6 | 7 | - `Data.HCL` exports the HCL parser 8 | - `Data.HCL.PrettyPrint` exports the HCL pretty-printer 9 | 10 | For `conf` parsers, see 11 | [**haskell-language-conf**](https://github.com/beijaflor-io/haskell-language-conf) 12 | 13 | ## Build & Test 14 | ``` 15 | make build 16 | ``` 17 | ``` 18 | make test 19 | ``` 20 | ``` 21 | make all 22 | ``` 23 | 24 | ## License 25 | Published under the MIT license 26 | -------------------------------------------------------------------------------- /Setup.hs: -------------------------------------------------------------------------------- 1 | import Distribution.Simple 2 | main = defaultMain 3 | -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | Vagrant.configure(2) do |config| 4 | config.vm.box = "ubuntu/trusty64" 5 | # config.vm.network "forwarded_port", guest: 80, host: 8080 6 | 7 | # Create a private network, which allows host-only access to the machine 8 | # using a specific IP. 9 | # config.vm.network "private_network", ip: "192.168.33.10" 10 | 11 | # Create a public network, which generally matched to bridged network. 12 | # Bridged networks make the machine appear as another physical device on 13 | # your network. 14 | # config.vm.network "public_network" 15 | 16 | # Share an additional folder to the guest VM. The first argument is 17 | # the path on the host to the actual folder. The second argument is 18 | # the path on the guest to mount the folder. And the optional third 19 | # argument is a set of non-required options. 20 | # config.vm.synced_folder "../data", "/vagrant_data" 21 | 22 | config.vm.provider "virtualbox" do |vb| 23 | # vb.gui = true 24 | vb.memory = "1024" 25 | end 26 | 27 | config.vm.provision "shell", inline: <<-SHELL 28 | sudo apt-get update 29 | sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 575159689BEFB442 30 | echo 'deb http://download.fpcomplete.com/ubuntu trusty main'|sudo tee /etc/apt/sources.list.d/fpco.list 31 | sudo apt-get update 32 | sudo apt-get install -y stack 33 | SHELL 34 | end 35 | -------------------------------------------------------------------------------- /bench/HCLBenchmark.hs: -------------------------------------------------------------------------------- 1 | import Control.Arrow ((>>>)) 2 | import Control.Monad 3 | import Criterion.Main 4 | import Data.HCL 5 | import qualified Data.Text.IO as Text (readFile) 6 | import System.Directory (getDirectoryContents) 7 | import System.FilePath (takeExtension, ()) 8 | 9 | main :: IO () 10 | main = do 11 | allFixtures <- 12 | filter (/= "unterminated_brace.hcl") . 13 | filter (/= "unterminated_block_comment.hcl") . 14 | filter (/= "multiline_no_marker.hcl") . 15 | filter (/= "multiline_bad.hcl") . 16 | filter (takeExtension >>> (== ".hcl")) <$> 17 | getDirectoryContents "./test-fixtures" 18 | 19 | bs <- forM allFixtures $ \fixture -> do 20 | print fixture 21 | input <- Text.readFile ("./test-fixtures" fixture) 22 | input `seq` return () 23 | return $ bench fixture $ nf 24 | (\i -> let Right h = parseHCL "" i in h) input 25 | 26 | defaultMain [ bgroup "parseHCL" bs 27 | ] 28 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | checkout: 2 | post: 3 | - git submodule sync 4 | - git submodule update --init --recursive || (rm -fr .git/config .git/modules && git submodule deinit -f . && git submodule update --init --recursive) 5 | 6 | dependencies: 7 | cache_directories: 8 | - "~/.stack" 9 | - ".stack" 10 | # - "cmake-3.2.2" 11 | # - "vendor/libui/build" 12 | pre: 13 | - sudo apt-get update 14 | - wget -q -O- https://s3.amazonaws.com/download.fpcomplete.com/ubuntu/fpco.key | sudo apt-key add - 15 | - echo 'deb http://download.fpcomplete.com/ubuntu/precise stable main'|sudo tee /etc/apt/sources.list.d/fpco.list 16 | - sudo apt-get update && sudo apt-get install stack -y 17 | - sudo apt-get install build-essential 18 | 19 | override: 20 | - stack build --only-dependencies --install-ghc --test 21 | 22 | test: 23 | pre: [] 24 | override: 25 | - make 26 | - stack test 27 | - cp -r `stack path --dist-dir` $CIRCLE_ARTIFACTS/ 28 | -------------------------------------------------------------------------------- /data/array_comment.hcl: -------------------------------------------------------------------------------- 1 | foo = [ 2 | "1", 3 | "2", # comment 4 | ] 5 | -------------------------------------------------------------------------------- /data/array_comment_2.hcl: -------------------------------------------------------------------------------- 1 | provisioner "remote-exec" { 2 | scripts = [ 3 | "${path.module}/scripts/install-consul.sh" // missing comma 4 | "${path.module}/scripts/install-haproxy.sh" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /data/assign_colon.hcl: -------------------------------------------------------------------------------- 1 | resource = [{ 2 | "foo": { 3 | "bar": {}, 4 | "baz": [1, 2, "foo"], 5 | } 6 | }] 7 | -------------------------------------------------------------------------------- /data/assign_deep.hcl: -------------------------------------------------------------------------------- 1 | resource = [{ 2 | foo = [{ 3 | bar = {} 4 | }] 5 | }] 6 | -------------------------------------------------------------------------------- /data/comment.hcl: -------------------------------------------------------------------------------- 1 | // Foo 2 | 3 | /* Bar */ 4 | 5 | /* 6 | /* 7 | Baz 8 | */ 9 | 10 | # Another 11 | 12 | # Multiple 13 | # Lines 14 | 15 | foo = "bar" 16 | -------------------------------------------------------------------------------- /data/comment_lastline.hcl: -------------------------------------------------------------------------------- 1 | #foo -------------------------------------------------------------------------------- /data/comment_single.hcl: -------------------------------------------------------------------------------- 1 | # Hello 2 | -------------------------------------------------------------------------------- /data/complex.hcl: -------------------------------------------------------------------------------- 1 | variable "foo" { 2 | default = "bar" 3 | description = "bar" 4 | } 5 | 6 | variable "groups" { } 7 | 8 | provider "aws" { 9 | access_key = "foo" 10 | secret_key = "bar" 11 | } 12 | 13 | provider "do" { 14 | api_key = "${var.foo}" 15 | } 16 | 17 | resource "aws_security_group" "firewall" { 18 | count = 5 19 | } 20 | 21 | resource aws_instance "web" { 22 | ami = "${var.foo}" 23 | security_groups = [ 24 | "foo", 25 | "${aws_security_group.firewall.foo}", 26 | "${element(split(\",\", var.groups)}", 27 | ] 28 | network_interface = { 29 | device_index = 0 30 | description = "Main network interface" 31 | } 32 | } 33 | 34 | resource "aws_instance" "db" { 35 | security_groups = "${aws_security_group.firewall.*.id}" 36 | VPC = "foo" 37 | depends_on = ["aws_instance.web"] 38 | } 39 | 40 | output "web_ip" { 41 | value = "${aws_instance.web.private_ip}" 42 | } 43 | -------------------------------------------------------------------------------- /data/complex_key.hcl: -------------------------------------------------------------------------------- 1 | foo.bar = "baz" 2 | -------------------------------------------------------------------------------- /data/empty.hcl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/beijaflor-io/haskell-language-hcl/a09011a1d193923390895c19c65544f99643a1ab/data/empty.hcl -------------------------------------------------------------------------------- /data/key_without_value.hcl: -------------------------------------------------------------------------------- 1 | foo 2 | -------------------------------------------------------------------------------- /data/list.hcl: -------------------------------------------------------------------------------- 1 | foo = [1, 2, "foo"] 2 | -------------------------------------------------------------------------------- /data/list_comma.hcl: -------------------------------------------------------------------------------- 1 | foo = [1, 2, "foo",] 2 | -------------------------------------------------------------------------------- /data/missing_braces.hcl: -------------------------------------------------------------------------------- 1 | # should error, but not crash 2 | resource "template_file" "cloud_config" { 3 | template = "$file("${path.module}/some/path")" 4 | } 5 | -------------------------------------------------------------------------------- /data/multiple.hcl: -------------------------------------------------------------------------------- 1 | foo = "bar" 2 | key = 7 3 | -------------------------------------------------------------------------------- /data/object_key_without_value.hcl: -------------------------------------------------------------------------------- 1 | foo { 2 | bar 3 | } 4 | -------------------------------------------------------------------------------- /data/old.hcl: -------------------------------------------------------------------------------- 1 | default = { 2 | "eu-west-1": "ami-b1cf19c6", 3 | } 4 | -------------------------------------------------------------------------------- /data/structure.hcl: -------------------------------------------------------------------------------- 1 | // This is a test structure for the lexer 2 | foo bar "baz" { 3 | key = 7 4 | foo = "bar" 5 | } 6 | -------------------------------------------------------------------------------- /data/structure_basic.hcl: -------------------------------------------------------------------------------- 1 | foo { 2 | value = 7 3 | "value" = 8 4 | "complex::value" = 9 5 | } 6 | -------------------------------------------------------------------------------- /data/structure_empty.hcl: -------------------------------------------------------------------------------- 1 | resource "foo" "bar" {} 2 | -------------------------------------------------------------------------------- /data/types.hcl: -------------------------------------------------------------------------------- 1 | foo = "bar" 2 | bar = 7 3 | baz = [1,2,3] 4 | foo = -12 5 | bar = 3.14159 6 | foo = true 7 | bar = false 8 | -------------------------------------------------------------------------------- /data/unterminated_object.hcl: -------------------------------------------------------------------------------- 1 | foo "baz" { 2 | bar = "baz" 3 | -------------------------------------------------------------------------------- /data/unterminated_object_2.hcl: -------------------------------------------------------------------------------- 1 | resource "aws_eip" "EIP1" { a { a { a { a { a { 2 | count = "1" 3 | 4 | resource "aws_eip" "EIP2" { 5 | count = "1" 6 | } 7 | -------------------------------------------------------------------------------- /default.conf: -------------------------------------------------------------------------------- 1 | #user nobody; 2 | worker_processes 1; 3 | 4 | #error_log logs/error.log; 5 | #error_log logs/error.log notice; 6 | #error_log logs/error.log info; 7 | 8 | #pid logs/nginx.pid; 9 | 10 | events { 11 | worker_connections 1024; 12 | } 13 | 14 | 15 | http { 16 | include mime.types; 17 | default_type application/octet-stream; 18 | 19 | #log_format main '$remote_addr - $remote_user [$time_local] "$request" ' 20 | # '$status $body_bytes_sent "$http_referer" ' 21 | # '"$http_user_agent" "$http_x_forwarded_for"'; 22 | 23 | #access_log logs/access.log main; 24 | 25 | sendfile on; 26 | #tcp_nopush on; 27 | 28 | #keepalive_timeout 0; 29 | keepalive_timeout 65; 30 | 31 | #gzip on; 32 | 33 | server { 34 | listen 9898; 35 | server_name localhost; 36 | 37 | #charset koi8-r; 38 | 39 | #access_log logs/host.access.log main; 40 | 41 | location / { 42 | root /usr/local/Library/Taps/railwaycat/homebrew-emacsmacport; 43 | index index.html index.htm; 44 | } 45 | 46 | #error_page 404 /404.html; 47 | 48 | # redirect server error pages to the static page /50x.html 49 | # 50 | error_page 500 502 503 504 /50x.html; 51 | location = /50x.html { 52 | root html; 53 | } 54 | 55 | # proxy the PHP scripts to Apache listening on 127.0.0.1:80 56 | # 57 | #location ~ \.php$ { 58 | # proxy_pass http://127.0.0.1; 59 | #} 60 | 61 | # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000 62 | # 63 | #location ~ \.php$ { 64 | # root html; 65 | # fastcgi_pass 127.0.0.1:9000; 66 | # fastcgi_index index.php; 67 | # fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name; 68 | # include fastcgi_params; 69 | #} 70 | 71 | # deny access to .htaccess files, if Apache's document root 72 | # concurs with nginx's one 73 | # 74 | #location ~ /\.ht { 75 | # deny all; 76 | #} 77 | } 78 | 79 | 80 | # another virtual host using mix of IP-, name-, and port-based configuration 81 | # 82 | #server { 83 | # listen 8000; 84 | # listen somename:8080; 85 | # server_name somename alias another.alias; 86 | 87 | # location / { 88 | # root html; 89 | # index index.html index.htm; 90 | # } 91 | #} 92 | 93 | 94 | # HTTPS server 95 | # 96 | #server { 97 | # listen 443 ssl; 98 | # server_name localhost; 99 | 100 | # ssl_certificate cert.pem; 101 | # ssl_certificate_key cert.key; 102 | 103 | # ssl_session_cache shared:SSL:1m; 104 | # ssl_session_timeout 5m; 105 | 106 | # ssl_ciphers HIGH:!aNULL:!MD5; 107 | # ssl_prefer_server_ciphers on; 108 | 109 | # location / { 110 | # root html; 111 | # index index.html index.htm; 112 | # } 113 | #} 114 | include servers/*; 115 | } 116 | -------------------------------------------------------------------------------- /package.yaml: -------------------------------------------------------------------------------- 1 | name: language-hcl 2 | version: '0.1.2.0' 3 | synopsis: HCL parsers and pretty-printers for the Haskell programming language. 4 | description: ! ' 5 | @language-hcl@ contains HCL (Hashicorp Configuration Language) parsers and 6 | pretty-printers for the Haskell programming language. 7 | 8 | "Data.HCL" exports all the API surface in the package. 9 | ' 10 | category: Data 11 | author: Pedro Tacla Yamada 12 | maintainer: tacla.yamada@gmail.com 13 | copyright: Copyright (c) 2016 Pedro Tacla Yamada 14 | license: MIT 15 | github: beijaflor-io/haskell-language-hcl 16 | extra-source-files: ./test-fixtures/* 17 | 18 | library: 19 | source-dirs: src 20 | exposed-modules: 21 | - Data.HCL 22 | - Data.HCL.Types 23 | other-modules: [] 24 | dependencies: 25 | - base >=4.8 && <5 26 | - deepseq 27 | - directory >=1.2.2.0 28 | - filepath >=1.4.0.0 29 | - megaparsec >= 6.2.0 30 | - prettyprinter >= 1.1.1 31 | - scientific >=0.3.4.6 32 | - semigroups >=0.18.1 33 | - text >=1.2.2.1 34 | - unordered-containers 35 | - void 36 | 37 | tests: 38 | hspec: 39 | main: Spec.hs 40 | source-dirs: test 41 | dependencies: 42 | - QuickCheck 43 | - base 44 | - directory 45 | - filepath 46 | - hspec 47 | - language-hcl 48 | - megaparsec >= 6.2.0 49 | - hspec-megaparsec 50 | - prettyprinter >= 1.1.1 51 | - semigroups >=0.18.1 52 | - text 53 | - transformers >=0.4.2.0 54 | 55 | benchmarks: 56 | hcl-benchmark: 57 | main: HCLBenchmark.hs 58 | source-dirs: bench 59 | ghc-options: 60 | - -O2 61 | - -threaded 62 | dependencies: 63 | - base 64 | - criterion 65 | - directory 66 | - filepath 67 | - language-hcl 68 | - text 69 | -------------------------------------------------------------------------------- /small.conf: -------------------------------------------------------------------------------- 1 | worker_processes 1; 2 | 3 | events { 4 | worker_connections 1024; 5 | } 6 | 7 | 8 | http { 9 | include mime.types; 10 | default_type application/octet-stream; 11 | sendfile on; 12 | keepalive_timeout 65; 13 | 14 | server { 15 | listen 9898; 16 | server_name localhost; 17 | 18 | location / { 19 | root /usr/local/Library/Taps/railwaycat/homebrew-emacsmacport; 20 | index index.html index.htm; 21 | } 22 | 23 | error_page 500 502 503 504 /50x.html; 24 | location = /50x.html { 25 | root html; 26 | } 27 | } 28 | 29 | include servers/*; 30 | } 31 | -------------------------------------------------------------------------------- /src/Data/HCL.hs: -------------------------------------------------------------------------------- 1 | {-| 2 | Module: Data.HCL 3 | Description: Exports a @.hcl@ Megaparsec parser through 'hcl' 4 | Copyright: (c) Copyright Pedro Tacla Yamada 2016 5 | License: MIT 6 | Maintainer: tacla.yamada@gmail.com 7 | Stability: experimental 8 | Portability: unknown 9 | 10 | This modules contains the 'hcl' Megaparsec parser for @hcl@ files. 11 | 12 | The pretty-printer is at "Data.HCL.PrettyPrint". 13 | -} 14 | {-# LANGUAGE OverloadedStrings #-} 15 | module Data.HCL 16 | ( 17 | -- * Entry-points 18 | parseHCL 19 | , hcl 20 | , runParser 21 | -- * Types 22 | , HCLDoc (..) 23 | , HCLStatement (..) 24 | , HCLValue (..) 25 | , HCLList (..) 26 | , HCLStringPart (..) 27 | -- * Pretty-printer 28 | , Pretty (..) 29 | -- * Support functions 30 | , topValue 31 | , bplain 32 | , binterp 33 | , string 34 | , stringParts 35 | , stringPart 36 | , stringPlain 37 | , stringPlainMultiline 38 | , stringInterp 39 | , assignment 40 | , object 41 | , value 42 | , ident 43 | , keys 44 | , key 45 | , list 46 | , number 47 | , Parser 48 | ) 49 | where 50 | 51 | import Control.Monad 52 | import qualified Data.HashMap.Strict as HashMap (fromList) 53 | import Data.Text (Text) 54 | import qualified Data.Text as Text 55 | import Data.Text.Prettyprint.Doc (Pretty) 56 | import Data.Void (Void) 57 | import Text.Megaparsec (ParseError (..), Parsec, eof, 58 | label, lookAhead, many, manyTill, 59 | optional, runParser, sepBy, sepBy1, 60 | skipMany, some, try, (<|>)) 61 | import Text.Megaparsec.Char (alphaNumChar, anyChar, char, eol, 62 | spaceChar, tab) 63 | import qualified Text.Megaparsec.Char as Megaparsec (string) 64 | import qualified Text.Megaparsec.Char.Lexer as Lexer 65 | 66 | import Data.HCL.Types 67 | 68 | 69 | type Parser = Parsec Void Text 70 | 71 | 72 | 73 | -- | 74 | -- Parser for the HCL format 75 | -- 76 | -- @ 77 | -- let h = runParser hcl fileName fileContents 78 | -- @ 79 | -- 80 | -- See "Text.Megaparsec" 81 | hcl :: Parser HCLDoc 82 | hcl = many $ do 83 | skipSpace 84 | topValue 85 | 86 | -- | 87 | -- Shortcut for @runParser 'hcl'@ 88 | parseHCL :: String -> Text -> Either (ParseError Char Void) HCLDoc 89 | parseHCL = runParser hcl 90 | 91 | topValue :: Parser HCLStatement 92 | topValue = label "HCL - topValue" $ 93 | HCLStatementObject <$> try object 94 | <|> HCLStatementAssignment <$> assignment 95 | 96 | value :: Parser HCLValue 97 | value = label "HCL - value" $ 98 | try object 99 | <|> HCLList <$> list 100 | <|> number 101 | <|> HCLIdent <$> ident 102 | <|> HCLString <$> stringParts 103 | <|> HCLString <$> (do s <- stringPlainMultiline; return [HCLStringPlain s]) 104 | 105 | object :: Parser HCLValue 106 | object = label "HCL - object" $ do 107 | ks <- keys 108 | skipSpace 109 | vchar '{' 110 | skipSpace 111 | fs <- manyTill assignment (vchar '}') 112 | skipSpace 113 | return $ HCLObject ks $ HashMap.fromList fs 114 | 115 | keys :: Parser [Text] 116 | keys = label "HCL - keys" $ many $ do 117 | k <- key 118 | skipSpace 119 | return k 120 | 121 | assignment :: Parser ([Text], HCLValue) 122 | assignment = label "HCL - assignment" $ do 123 | i <- sepBy1 ident (char '.') 124 | skipSpace 125 | vchar '=' 126 | skipSpace 127 | v <- value 128 | skipSpace 129 | return (i, v) 130 | 131 | vchar :: Char -> Parser () 132 | vchar = 133 | void . char 134 | 135 | key :: Parser Text 136 | key = string <|> ident 137 | 138 | list :: Parser HCLList 139 | list = do 140 | vchar '[' 141 | skipSpace 142 | vs <- value `sepBy` (skipSpace >> comma >> skipSpace) 143 | skipSpace 144 | _ <- optional comma 145 | skipSpace 146 | vchar ']' 147 | return vs 148 | 149 | comma :: Parser () 150 | comma = 151 | vchar ',' 152 | 153 | -- quote :: Parser () 154 | quote :: Parser Text 155 | quote = Lexer.symbol skipSpace "\"" 156 | 157 | bplain :: Text -> HCLValue 158 | bplain s = HCLString [HCLStringPlain s] 159 | 160 | binterp :: Text -> HCLValue 161 | binterp s = HCLString [HCLStringInterp s] 162 | 163 | stringParts :: Parser [HCLStringPart] 164 | stringParts = label "HCL - stringParts" $ do 165 | _ <- quote 166 | manyTill stringPart quote 167 | 168 | stringPart :: Parser HCLStringPart 169 | stringPart = label "HCL - stringPart" $ 170 | try (HCLStringInterp <$> stringInterp) 171 | <|> HCLStringPlain <$> stringPlain 172 | 173 | stringInterp :: Parser Text 174 | stringInterp = label "HCL - stringInterp" $ do 175 | _ <- Lexer.symbol skipSpace "${" 176 | Text.pack <$> manyTill anyChar (Megaparsec.string "}") 177 | 178 | stringPlain :: Parser Text 179 | stringPlain = label "HCL - stringPlain" $ do 180 | let end = 181 | try (lookAhead eof) 182 | <|> void (try (lookAhead (Megaparsec.string "${"))) 183 | <|> void (try (lookAhead quote)) 184 | s <- manyTill Lexer.charLiteral end 185 | return $ Text.pack s 186 | 187 | stringPlainMultiline :: Parser Text 188 | stringPlainMultiline = label "HCL - stringPlainMultiline" $ do 189 | _ <- Megaparsec.string "<<" 190 | _ <- optional (char '-') 191 | _ <- Megaparsec.string "EOF" 192 | _ <- eol 193 | Text.pack <$> manyTill Lexer.charLiteral 194 | (try (skipSpace >> Megaparsec.string "EOF")) 195 | 196 | string :: Parser Text 197 | string = label "HCL - string" $ try stringPlainMultiline <|> str 198 | where 199 | str = do 200 | _ <- quote 201 | s <- manyTill Lexer.charLiteral quote 202 | return $ Text.pack s 203 | 204 | number :: Parser HCLValue 205 | number = 206 | HCLNumber <$> Lexer.scientific 207 | 208 | ident :: Parser Text 209 | ident = Text.pack <$> some (alphaNumChar <|> char '_' <|> char '-') 210 | 211 | skipSpace :: Parser () 212 | skipSpace = skipMany $ 213 | skipLineComment 214 | <|> skipBlockComment 215 | <|> void eol 216 | <|> void spaceChar 217 | <|> void tab 218 | 219 | skipLineComment :: Parser () 220 | skipLineComment = 221 | Lexer.skipLineComment "#" 222 | <|> Lexer.skipLineComment "//" 223 | 224 | skipBlockComment :: Parser () 225 | skipBlockComment = 226 | Lexer.skipBlockComment "/*" "*/" 227 | -------------------------------------------------------------------------------- /src/Data/HCL/Types.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE DeriveAnyClass #-} 2 | {-# LANGUAGE DeriveGeneric #-} 3 | {-# LANGUAGE OverloadedStrings #-} 4 | 5 | module Data.HCL.Types where 6 | 7 | import Control.DeepSeq (NFData) 8 | import Data.HashMap.Strict (HashMap, toList) 9 | import Data.Monoid ((<>)) 10 | import Data.Scientific (Scientific, toRealFloat) 11 | import Data.Text (Text) 12 | import qualified Data.Text as Text 13 | import Data.Text.Prettyprint.Doc (Doc, Pretty, align, annotate, comma, 14 | defaultLayoutOptions, dot, dquotes, 15 | encloseSep, fill, hsep, 16 | layoutPretty, nest, pretty, 17 | prettyList, punctuate, unAnnotate, 18 | vsep, (<+>)) 19 | import GHC.Generics (Generic) 20 | 21 | 22 | 23 | 24 | -- | The HCL document is just a list of statements 25 | type HCLDoc = [HCLStatement] 26 | 27 | -- | Statements may be "objects", of form: 28 | -- 29 | -- @ 30 | -- provider "aws" { 31 | -- # more 32 | -- } 33 | -- @ 34 | -- 35 | -- Or they may be assignments: 36 | -- 37 | -- @ 38 | -- a = "b" 39 | -- @ 40 | data HCLStatement = HCLStatementObject HCLValue 41 | | HCLStatementAssignment ([Text], HCLValue) 42 | deriving(Generic, Show, Eq, NFData) 43 | 44 | data HCLValue = HCLNumber Scientific 45 | | HCLString [HCLStringPart] 46 | | HCLIdent Text 47 | | HCLObject [Text] (HashMap [Text] HCLValue) 48 | | HCLList [HCLValue] 49 | deriving(Generic, Show, Eq, NFData) 50 | 51 | type HCLList = [HCLValue] 52 | 53 | data HCLStringPart = HCLStringPlain Text 54 | | HCLStringInterp Text 55 | deriving(Generic, Show, Eq, NFData) 56 | 57 | 58 | 59 | instance Pretty HCLStatement where 60 | pretty s = case s of 61 | HCLStatementObject o -> pretty o 62 | HCLStatementAssignment (is, v) -> 63 | encloseSep "" "" dot (pretty <$> is) <+> "=" <+> pretty v 64 | prettyList = vsep . fmap pretty 65 | 66 | instance Pretty HCLValue where 67 | pretty v = case v of 68 | HCLNumber n -> pretty $ (toRealFloat n :: Double) 69 | HCLString ps -> hsep $ pretty <$> ps 70 | HCLIdent t -> pretty t 71 | HCLObject ks h -> vsep $ [(hsep $ prettyKey <$> ks) <+> "{"] <> prettyFields (toList h) <> ["}"] 72 | HCLList vs -> "[" <> (hsep $ punctuate comma (pretty <$> vs)) <> "]" 73 | 74 | instance Pretty HCLStringPart where 75 | pretty s = case s of 76 | HCLStringPlain t -> dquotes $ pretty t 77 | HCLStringInterp t -> "#{" <> pretty t <> "}" 78 | 79 | 80 | prettyKey :: Text -> Doc ann 81 | prettyKey t | Text.any (==' ') t = dquotes $ pretty t 82 | | otherwise = pretty t 83 | 84 | prettyFields :: [([Text], HCLValue)] -> [Doc ann] 85 | prettyFields = fmap ((" " <>) . pretty . HCLStatementAssignment) 86 | 87 | 88 | -------------------------------------------------------------------------------- /stack.yaml: -------------------------------------------------------------------------------- 1 | resolver: nightly-2017-10-13 2 | 3 | packages: 4 | - '.' 5 | extra-deps: [] 6 | 7 | flags: {} 8 | 9 | extra-package-dbs: [] 10 | 11 | -------------------------------------------------------------------------------- /test-fixtures/basic.hcl: -------------------------------------------------------------------------------- 1 | foo = "bar" 2 | bar = "${file("bing/bong.txt")}" 3 | -------------------------------------------------------------------------------- /test-fixtures/basic.json: -------------------------------------------------------------------------------- 1 | { 2 | "foo": "bar", 3 | "bar": "${file(\"bing/bong.txt\")}" 4 | } 5 | -------------------------------------------------------------------------------- /test-fixtures/basic_int_string.hcl: -------------------------------------------------------------------------------- 1 | count = "3" 2 | -------------------------------------------------------------------------------- /test-fixtures/basic_squish.hcl: -------------------------------------------------------------------------------- 1 | foo="bar" 2 | bar="${file("bing/bong.txt")}" 3 | foo-bar="baz" 4 | -------------------------------------------------------------------------------- /test-fixtures/decode_policy.hcl: -------------------------------------------------------------------------------- 1 | key "" { 2 | policy = "read" 3 | } 4 | 5 | key "foo/" { 6 | policy = "write" 7 | } 8 | 9 | key "foo/bar/" { 10 | policy = "read" 11 | } 12 | 13 | key "foo/bar/baz" { 14 | policy = "deny" 15 | } 16 | -------------------------------------------------------------------------------- /test-fixtures/decode_policy.json: -------------------------------------------------------------------------------- 1 | { 2 | "key": { 3 | "": { 4 | "policy": "read" 5 | }, 6 | 7 | "foo/": { 8 | "policy": "write" 9 | }, 10 | 11 | "foo/bar/": { 12 | "policy": "read" 13 | }, 14 | 15 | "foo/bar/baz": { 16 | "policy": "deny" 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /test-fixtures/decode_tf_variable.hcl: -------------------------------------------------------------------------------- 1 | variable "foo" { 2 | default = "bar" 3 | description = "bar" 4 | } 5 | 6 | variable "amis" { 7 | default = { 8 | east = "foo" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test-fixtures/decode_tf_variable.json: -------------------------------------------------------------------------------- 1 | { 2 | "variable": { 3 | "foo": { 4 | "default": "bar", 5 | "description": "bar" 6 | }, 7 | 8 | "amis": { 9 | "default": { 10 | "east": "foo" 11 | } 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /test-fixtures/empty.hcl: -------------------------------------------------------------------------------- 1 | resource "foo" {} 2 | -------------------------------------------------------------------------------- /test-fixtures/escape.hcl: -------------------------------------------------------------------------------- 1 | foo = "bar\"baz\\n" 2 | -------------------------------------------------------------------------------- /test-fixtures/flat.hcl: -------------------------------------------------------------------------------- 1 | foo = "bar" 2 | Key = 7 3 | -------------------------------------------------------------------------------- /test-fixtures/float.hcl: -------------------------------------------------------------------------------- 1 | a = 1.02 2 | -------------------------------------------------------------------------------- /test-fixtures/float.json: -------------------------------------------------------------------------------- 1 | { 2 | "a": 1.02 3 | } 4 | -------------------------------------------------------------------------------- /test-fixtures/interpolate_escape.hcl: -------------------------------------------------------------------------------- 1 | foo="${file(\"bing/bong.txt\")}" 2 | -------------------------------------------------------------------------------- /test-fixtures/multiline.hcl: -------------------------------------------------------------------------------- 1 | foo = < Text -> Text -> Expectation 11 | -- testParser p i o = case runParser p "" i of 12 | -- Left e -> error (show e) 13 | -- Right a -> a `shouldBe` o 14 | 15 | -- testFailure :: FilePath -> Parser a -> Text -> Expectation 16 | -- testFailure fp p i = case runParser p fp i of 17 | -- Right _ -> error "This should have failed" 18 | -- _ -> True `shouldBe` True 19 | 20 | testParser 21 | :: (Eq a, Show e, Show a, Show (Token s)) 22 | => Parsec e s a -> s -> a -> Expectation 23 | testParser p i o = case runParser p "" i of 24 | Left e -> error (show e) 25 | Right a -> a `shouldBe` o 26 | 27 | testFailure :: String -> Text -> Expectation 28 | testFailure fp inp = case parseHCL fp inp of 29 | Right _ -> error "This should have failed" 30 | _ -> True `shouldBe` True 31 | 32 | testFailureP :: Parser Text -> String -> Text -> Expectation 33 | testFailureP p fp inp = case runParser p fp inp of 34 | Right _ -> error "This should have failed" 35 | _ -> True `shouldBe` True 36 | -------------------------------------------------------------------------------- /test/Data/HCLSpec.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE FlexibleContexts #-} 2 | {-# LANGUAGE OverloadedLists #-} 3 | {-# LANGUAGE OverloadedStrings #-} 4 | module Data.HCLSpec where 5 | 6 | import Control.Monad 7 | import Control.Monad.IO.Class 8 | import Data.HCL 9 | import Data.HCL.TestHelper 10 | import Data.Text (Text) 11 | import qualified Data.Text as Text 12 | import qualified Data.Text.IO as Text 13 | import System.Directory 14 | import System.FilePath 15 | import System.IO.Unsafe 16 | import Test.Hspec 17 | import Text.Megaparsec (Parsec, Token (..), runParser) 18 | 19 | {-# NOINLINE fs' #-} 20 | fs' :: [FilePath] 21 | fs' = unsafePerformIO $ do 22 | fs <- liftIO (getDirectoryContents "./test-fixtures") 23 | return $ filter ((== ".hcl") . takeExtension) fs 24 | 25 | spec :: Spec 26 | spec = do 27 | describe "stringParts" $ do 28 | it "parses normal strings" $ do 29 | let input = "\"something\"" 30 | testParser stringParts input [HCLStringPlain "something"] 31 | 32 | it "parses interpolated strings" $ do 33 | let input = "\"${asdf} Hello World asdfasdf ${hey}\"" 34 | testParser stringParts input [ HCLStringInterp "asdf" 35 | , HCLStringPlain " Hello World asdfasdf " 36 | , HCLStringInterp "hey" 37 | ] 38 | 39 | describe "ident" $ do 40 | it "parses alphanum" $ do 41 | let input = "asdf" 42 | testParser ident input "asdf" 43 | 44 | it "parses dashes" $ do 45 | let input = "asdf-asdf" 46 | testParser ident input "asdf-asdf" 47 | 48 | it "parses underscores" $ do 49 | let input = "asdf_asdf" 50 | testParser ident input "asdf_asdf" 51 | 52 | it "stops at whitespace" $ do 53 | let input = "asdf asdf" 54 | testParser ident input "asdf" 55 | 56 | describe "stringPlain" $ do 57 | it "parses the empty string" $ do 58 | let input = "" 59 | testParser stringPlain input "" 60 | 61 | it "parses charaters" $ do 62 | let input = "something" 63 | testParser stringPlain input "something" 64 | 65 | it "parses escape sequences" $ do 66 | let input = "bar\\\"baz\\n" 67 | testParser stringPlain input "bar\"baz\n" 68 | 69 | describe "stringPlainMultiline" $ 70 | it "parses multiline strings" $ do 71 | let input = Text.unlines [ "< it fp $ do 166 | inp <- liftIO $ Text.readFile ("test-fixtures" fp) 167 | case fp of 168 | "unterminated_block_comment.hcl" -> testFailure fp inp 169 | "multiline_no_marker.hcl" -> testFailure fp inp 170 | "multiline_bad.hcl" -> testFailure fp inp 171 | "unterminated_brace.hcl" -> testFailure fp inp 172 | _ -> case parseHCL fp inp of 173 | Left e -> error (show e) 174 | Right _ -> True `shouldBe` True 175 | -------------------------------------------------------------------------------- /test/SanitySpec.hs: -------------------------------------------------------------------------------- 1 | module SanitySpec where 2 | 3 | import Test.Hspec 4 | 5 | spec = describe "when I have tests" $ 6 | it "I have sanity" $ True `shouldBe` True 7 | -------------------------------------------------------------------------------- /test/Spec.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS_GHC -F -pgmF hspec-discover #-} --------------------------------------------------------------------------------