├── test ├── fixtures │ ├── multi.yaml │ └── petstore.yaml └── yaml │ ├── core_test.clj │ ├── writer_test.clj │ └── reader_test.clj ├── .gitignore ├── Makefile ├── project.clj ├── src └── yaml │ ├── core.clj │ ├── reader.clj │ └── writer.clj ├── LICENSE ├── src-java └── org │ └── yaml │ └── snakeyaml │ └── constructor ├── .circleci └── config.yml └── README.md /test/fixtures/multi.yaml: -------------------------------------------------------------------------------- 1 | # YAML 2 | --- 3 | document: this is doc 1 4 | --- 5 | document: this is doc 2 6 | ... -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /classes 3 | /checkouts 4 | pom.xml 5 | pom.xml.asc 6 | *.jar 7 | *.class 8 | /.lein-* 9 | /.nrepl-port 10 | .hgignore 11 | .hg/ 12 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | VERSION := $(shell head -1 project.clj | cut -d " " -f 3 | tr -d '"') 2 | 3 | PHONY: test 4 | test: 5 | lein test 6 | 7 | .PHONY: tag 8 | tag: 9 | git tag -a $(VERSION) -m "Release $(VERSION)" 10 | git push origin $(VERSION) 11 | 12 | .PHONY: deploy 13 | deploy: 14 | @GPG_TTY=$(tty) lein deploy clojars 15 | 16 | .PHONY: release 17 | release: 18 | deploy tag 19 | -------------------------------------------------------------------------------- /test/yaml/core_test.clj: -------------------------------------------------------------------------------- 1 | (ns yaml.core-test 2 | (:require [clojure.test :refer :all] 3 | [yaml.core :as yaml])) 4 | 5 | (deftest from-file-test 6 | (testing "should parse a single document" 7 | (let [yml (yaml/from-file "test/fixtures/petstore.yaml" true)] 8 | (is (= (:schemes yml) ["http"])))) 9 | (testing "should parse multiple documents" 10 | (let [yml (yaml/from-file "test/fixtures/multi.yaml" true)] 11 | (is (vector? yml))))) 12 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject com.owainlewis/yaml "1.0.11-SNAPSHOT" 2 | :description "A YAML library for Clojure" 3 | :url "http://github.com/owainlewis/yaml" 4 | :license {:name "Eclipse Public License" 5 | :url "http://www.eclipse.org/legal/epl-v10.html"} 6 | :dependencies [[org.yaml/snakeyaml "1.33"] 7 | [org.flatland/ordered "1.5.7"]] 8 | :deploy-repositories [["clojars" {:url "https://clojars.org/repo" 9 | :username :env/clojars_username 10 | :password :env/clojars_password 11 | :sign-releases false}]] 12 | :java-source-paths ["src-java"]) 13 | -------------------------------------------------------------------------------- /src/yaml/core.clj: -------------------------------------------------------------------------------- 1 | (ns yaml.core 2 | (:require [clojure.java.io :as io] 3 | [yaml.reader :as reader] 4 | [yaml.writer :as writer])) 5 | 6 | 7 | (def generate-string 8 | writer/generate-string) 9 | 10 | (def parse-string 11 | reader/parse-string) 12 | 13 | (defn- safe-read 14 | "Try and read a file. If it does not exist then return nil rather 15 | than an exception" 16 | [f] 17 | (when (.exists (io/file f)) 18 | (slurp f))) 19 | 20 | (defn from-file 21 | "Reads a YAML file and returns the decoded result" 22 | ([f] 23 | (from-file f true)) 24 | ([f keywords] 25 | (when-let [contents (safe-read f)] 26 | (parse-string contents :keywords keywords)))) 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Owain Lewis 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src-java/org/yaml/snakeyaml/constructor/PassthroughConstructor.java: -------------------------------------------------------------------------------- 1 | package org.yaml.snakeyaml.constructor; 2 | 3 | import org.yaml.snakeyaml.nodes.Node; 4 | import org.yaml.snakeyaml.nodes.ScalarNode; 5 | import org.yaml.snakeyaml.nodes.Tag; 6 | 7 | /** 8 | * Implementation of Constructor that ignores YAML tags. 9 | * 10 | * This is used as a fallback strategies to use the underlying type instead of 11 | * throwing an exception. 12 | */ 13 | public class PassthroughConstructor extends Constructor { 14 | 15 | private class PassthroughConstruct extends AbstractConstruct { 16 | public Object construct(Node node) { 17 | // reset node to scalar tag type for parsing 18 | Tag tag = null; 19 | switch (node.getNodeId()) { 20 | case scalar: 21 | tag = Tag.STR; 22 | break; 23 | case sequence: 24 | tag = Tag.SEQ; 25 | break; 26 | default: 27 | tag = Tag.MAP; 28 | break; 29 | } 30 | 31 | node.setTag(tag); 32 | return getConstructor(node).construct(node); 33 | } 34 | 35 | public void construct2ndStep(Node node, Object object) {} 36 | } 37 | 38 | public PassthroughConstructor() { 39 | // Add a catch-all to catch any unidentifiable nodes 40 | this.yamlMultiConstructors.put("", new PassthroughConstruct()); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | jobs: 3 | test: 4 | docker: 5 | - image: clojure:openjdk-11-lein-2.9.1 6 | working_directory: /home/circleci/owainlewis/yaml 7 | environment: 8 | LEIN_ROOT: "true" 9 | JVM_OPTS: -Xmx3200m 10 | steps: 11 | - attach_workspace: 12 | at: /home/circleci 13 | - checkout 14 | # Download and cache dependencies 15 | - restore_cache: 16 | keys: 17 | - v1-dependencies-{{ checksum "project.clj" }} 18 | # fallback to using the latest cache if no exact match is found 19 | - v1-dependencies- 20 | - run: lein deps 21 | - save_cache: 22 | paths: 23 | - /home/circleci/.m2 24 | key: v1-dependencies-{{ checksum "project.clj" }} 25 | - persist_to_workspace: 26 | root: /home/circleci/ 27 | paths: 28 | - .m2 29 | - owainlewis/yaml 30 | # run tests! 31 | - run: lein with-profiles dev test 32 | deploy: 33 | working_directory: /home/circleci/owainlewis/yaml 34 | docker: 35 | - image: clojure:openjdk-11-lein-2.9.1 36 | steps: 37 | - attach_workspace: 38 | at: /home/circleci 39 | - run: 40 | name: Deploy to clojars 41 | command: CLOJARS_USERNAME=$CLOJARS_USERNAME CLOJARS_PASSWORD=$CLOJARS_PASSWORD lein deploy clojars 42 | 43 | workflows: 44 | version: 2 45 | build: 46 | jobs: 47 | - test 48 | - deploy: 49 | requires: 50 | - test 51 | filters: 52 | branches: 53 | only: master 54 | -------------------------------------------------------------------------------- /test/yaml/writer_test.clj: -------------------------------------------------------------------------------- 1 | (ns yaml.writer-test 2 | (:require [clojure.test :refer :all] 3 | [yaml.writer :refer :all] 4 | [flatland.ordered.set :refer [ordered-set]] 5 | [flatland.ordered.map :refer [ordered-map]])) 6 | 7 | (deftest dump-opts 8 | (let [data [{:age 33 :name "jon"} {:age 44 :name "boo"}]] 9 | 10 | (is (= "- age: 33\n name: jon\n- age: 44\n name: boo\n" 11 | (generate-string data :dumper-options {:flow-style :block}))) 12 | (is (= "[{age: 33, name: jon}, {age: 44, name: boo}]\n" 13 | (generate-string data :dumper-options {:flow-style :flow}))) 14 | 15 | (is (= "- \"age\": !!int \"33\"\n \"name\": \"jon\"\n- \"age\": !!int \"44\"\n \"name\": \"boo\"\n" 16 | (generate-string data :dumper-options {:scalar-style :double-quoted}))) 17 | (is (= "- 'age': !!int '33'\n 'name': 'jon'\n- 'age': !!int '44'\n 'name': 'boo'\n" 18 | (generate-string data :dumper-options {:scalar-style :single-quoted}))) 19 | (is (= "- \"age\": !!int |-\n 33\n \"name\": |-\n jon\n- \"age\": !!int |-\n 44\n \"name\": |-\n boo\n" 20 | (generate-string data :dumper-options {:scalar-style :literal}))) 21 | (is (= "- \"age\": !!int >-\n 33\n \"name\": >-\n jon\n- \"age\": !!int >-\n 44\n \"name\": >-\n boo\n" 22 | (generate-string data :dumper-options {:scalar-style :folded}))) 23 | (is (= "- {age: 33, name: jon}\n- {age: 44, name: boo}\n" 24 | (generate-string data :dumper-options {:scalar-style :plain}))))) 25 | 26 | (deftest preserve-namespaces 27 | (let [data {:foo/bar "baz"}] 28 | (is (= "{foo/bar: baz}\n" 29 | (generate-string data))))) 30 | 31 | (deftest writing-order 32 | (let [om (into (ordered-map) (partition 2 (range 0 20))) 33 | os (into (ordered-set) (range 0 10)) 34 | v (into [] (range 0 10))] 35 | (= "0: 1\n2: 3\n4: 5\n6: 7\n8: 9\n10: 11\n12: 13\n14: 15\n16: 17\n18: 19\n" 36 | (generate-string om :dumper-options {:flow-style :block})) 37 | (= "!!set\n0: null\n1: null\n2: null\n3: null\n4: null\n5: null\n6: null\n7: null\n8: null\n9: null\n" 38 | (generate-string os :dumper-options {:flow-style :block})) 39 | (= "- 0\n- 1\n- 2\n- 3\n- 4\n- 5\n- 6\n- 7\n- 8\n- 9\n" 40 | (generate-string v :dumper-options {:flow-style :block})))) 41 | -------------------------------------------------------------------------------- /test/fixtures/petstore.yaml: -------------------------------------------------------------------------------- 1 | swagger: "2.0" 2 | info: 3 | version: 1.0.0 4 | title: Swagger Petstore 5 | license: 6 | name: MIT 7 | host: petstore.swagger.io 8 | basePath: /v1 9 | schemes: 10 | - http 11 | consumes: 12 | - application/json 13 | produces: 14 | - application/json 15 | paths: 16 | /pets: 17 | get: 18 | summary: List all pets 19 | operationId: listPets 20 | tags: 21 | - pets 22 | parameters: 23 | - name: limit 24 | in: query 25 | description: How many items to return at one time (max 100) 26 | required: false 27 | type: integer 28 | format: int32 29 | responses: 30 | 200: 31 | description: An paged array of pets 32 | headers: 33 | x-next: 34 | type: string 35 | description: A link to the next page of responses 36 | schema: 37 | $ref: Pets 38 | default: 39 | description: unexpected error 40 | schema: 41 | $ref: Error 42 | post: 43 | summary: Create a pet 44 | operationId: createPets 45 | tags: 46 | - pets 47 | responses: 48 | 201: 49 | description: Null response 50 | default: 51 | description: unexpected error 52 | schema: 53 | $ref: Error 54 | /pets/{petId}: 55 | get: 56 | summary: Info for a specific pet 57 | operationId: showPetById 58 | tags: 59 | - pets 60 | parameters: 61 | - name: petId 62 | in: path 63 | required: true 64 | description: The id of the pet to retrieve 65 | type: string 66 | responses: 67 | 200: 68 | description: Expected response to a valid request 69 | schema: 70 | $ref: Pets 71 | default: 72 | description: unexpected error 73 | schema: 74 | $ref: Error 75 | definitions: 76 | Pet: 77 | required: 78 | - id 79 | - name 80 | properties: 81 | id: 82 | type: integer 83 | format: int64 84 | name: 85 | type: string 86 | tag: 87 | type: string 88 | Pets: 89 | type: array 90 | items: 91 | $ref: Pet 92 | Error: 93 | required: 94 | - code 95 | - message 96 | properties: 97 | code: 98 | type: integer 99 | format: int32 100 | message: 101 | type: string 102 | -------------------------------------------------------------------------------- /src/yaml/reader.clj: -------------------------------------------------------------------------------- 1 | (ns yaml.reader 2 | (:require [flatland.ordered.set :refer [ordered-set]] 3 | [flatland.ordered.map :refer [ordered-map]]) 4 | (:refer-clojure :exclude [load]) 5 | (:import [org.yaml.snakeyaml Yaml] 6 | [org.yaml.snakeyaml.constructor Constructor PassthroughConstructor] 7 | [org.yaml.snakeyaml.composer ComposerException])) 8 | 9 | (def ^:dynamic *keywordize* true) 10 | (def ^:dynamic *constructor* (fn [] (Constructor.))) 11 | (def passthrough-constructor 12 | "Custom constructor that will not barf on unknown YAML tags. This constructor 13 | will treat YAML objects with unknown tags with the underlying type (i.e. map, 14 | sequence, scalar) " 15 | (fn [] (PassthroughConstructor.))) 16 | 17 | (defprotocol YAMLReader 18 | (decode [data])) 19 | 20 | (defn- decode-key 21 | "When *keywordize* is bound to true decode map keys into keywords else leave them 22 | as strings. When *keywordize* is a function, calls function on the key." 23 | [k] 24 | (cond (true? *keywordize*) (keyword k) 25 | (fn? *keywordize*) (*keywordize* k) 26 | :else k)) 27 | 28 | (extend-protocol YAMLReader 29 | java.util.LinkedHashMap 30 | (decode [data] 31 | (into (ordered-map) 32 | (for [[k v] data] 33 | [(decode-key k) (decode v)]))) 34 | java.util.LinkedHashSet 35 | (decode [data] 36 | (into (ordered-set) data)) 37 | java.util.ArrayList 38 | (decode [data] 39 | (into [] 40 | (map decode data))) 41 | Object 42 | (decode [data] data) 43 | nil 44 | (decode [data] data)) 45 | 46 | (defn parse-documents 47 | "The YAML spec allows for multiple documents. This will take a string containing multiple yaml 48 | docs and return a vector containing each document" 49 | [^String yaml-documents] 50 | (mapv decode 51 | (.loadAll (Yaml. (*constructor*)) yaml-documents))) 52 | 53 | (defn parse-string 54 | "Parse a yaml input string. If multiple documents are found it will return a vector of documents 55 | 56 | When keywords is a true (default), map keys are converted to keywords. When 57 | keywords is a function, invokes the function on the map keys. 58 | 59 | When a custom :constructor is provided, it's used to construct objects. Should 60 | be a 0-arity function that returns a constructor. 61 | " 62 | [^String string & {:keys [keywords constructor] 63 | :or {keywords *keywordize* 64 | constructor *constructor*}}] 65 | (binding [*keywordize* keywords] 66 | (try 67 | (decode (.load (Yaml. (constructor)) string)) 68 | (catch ComposerException e 69 | (parse-documents string))))) 70 | -------------------------------------------------------------------------------- /src/yaml/writer.clj: -------------------------------------------------------------------------------- 1 | (ns yaml.writer 2 | (:require [flatland.ordered.set :refer [ordered-set]] 3 | [flatland.ordered.map :refer [ordered-map]]) 4 | (:import (org.yaml.snakeyaml Yaml DumperOptions DumperOptions$FlowStyle DumperOptions$ScalarStyle))) 5 | 6 | (def flow-styles 7 | {:auto DumperOptions$FlowStyle/AUTO 8 | :block DumperOptions$FlowStyle/BLOCK 9 | :flow DumperOptions$FlowStyle/FLOW}) 10 | 11 | (def scalar-styles 12 | {:double-quoted DumperOptions$ScalarStyle/DOUBLE_QUOTED 13 | :single-quoted DumperOptions$ScalarStyle/SINGLE_QUOTED 14 | :literal DumperOptions$ScalarStyle/LITERAL 15 | :folded DumperOptions$ScalarStyle/FOLDED 16 | :plain DumperOptions$ScalarStyle/PLAIN}) 17 | 18 | (defn- make-dumper-options 19 | [{:keys [flow-style scalar-style split-lines width]}] 20 | (let [options (DumperOptions.)] 21 | (when flow-style 22 | (.setDefaultFlowStyle options (flow-styles flow-style))) 23 | (when scalar-style 24 | (.setDefaultScalarStyle options (scalar-styles scalar-style))) 25 | (when (some? split-lines) 26 | (.setSplitLines options split-lines)) 27 | (when (some? width) 28 | (.setWidth options width)) 29 | options)) 30 | 31 | (defn make-yaml 32 | [& {:keys [dumper-options]}] 33 | (if dumper-options 34 | (Yaml. ^DumperOptions (make-dumper-options dumper-options)) 35 | (Yaml.))) 36 | 37 | (defn- keyword->string 38 | [key] 39 | (if (nil? (namespace key)) 40 | (name key) 41 | (str (namespace key) "/" (name key)))) 42 | 43 | (defprotocol YAMLWriter 44 | (encode [data])) 45 | 46 | (extend-protocol YAMLWriter 47 | flatland.ordered.set.OrderedSet 48 | (encode [data] 49 | (java.util.LinkedHashSet. 50 | ^flatland.ordered.set.OrderedSet 51 | (into (ordered-set) 52 | (map encode data)))) 53 | flatland.ordered.map.OrderedMap 54 | (encode [data] 55 | (java.util.LinkedHashMap. 56 | ^flatland.ordered.map.OrderedMap 57 | (into (ordered-map) 58 | (for [[k v] data] 59 | [(encode k) (encode v)])))) 60 | clojure.lang.IPersistentMap 61 | (encode [data] 62 | (into {} 63 | (for [[k v] data] 64 | [(encode k) (encode v)]))) 65 | clojure.lang.IPersistentSet 66 | (encode [data] 67 | (into #{} 68 | (map encode data))) 69 | clojure.lang.IPersistentCollection 70 | (encode [data] 71 | (map encode data)) 72 | clojure.lang.PersistentTreeMap 73 | (encode [data] 74 | (into (sorted-map) 75 | (for [[k v] data] 76 | [(encode k) (encode v)]))) 77 | clojure.lang.Keyword 78 | (encode [data] 79 | (keyword->string data)) 80 | Object 81 | (encode [data] data) 82 | nil 83 | (encode [data] data)) 84 | 85 | (defn generate-string [data & opts] 86 | (.dump ^Yaml (apply make-yaml opts) 87 | ^Object (encode data))) 88 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # YAML 2 | 3 | CI Build Status: 4 | 5 | [![CircleCI](https://circleci.com/gh/owainlewis/yaml/tree/master.svg?style=svg)](https://circleci.com/gh/owainlewis/yaml/tree/master) 6 | 7 | ### About 8 | An updated YAML library for Clojure based on Snake YAML and heavily inspired by clj-yaml 9 | 10 | ## Install 11 | 12 | ### Lein 13 | 14 | [![Clojars Project](http://clojars.org/io.forward/yaml/latest-version.svg)](http://clojars.org/io.forward/yaml) 15 | 16 | ## Usage 17 | 18 | ```clojure 19 | (ns demo.core 20 | (:refer-clojure :exclude [load]) 21 | (:require [yaml.core :as yaml])) 22 | 23 | ;; Note on DSL 24 | ;; yaml/load & yaml/parse-string are identical 25 | ;; yaml/dump & yaml/generate-string are identical 26 | 27 | ;; Parse a YAML file 28 | 29 | (yaml/from-file "config.yml") 30 | 31 | ;; Parse a YAML string 32 | 33 | (yaml/parse-string "foo: bar") 34 | 35 | ;; Optionally pass `true` as a second argument to from-file or parse-string to keywordize all keys 36 | (yaml/parse-string "foo: bar" :keywords true) 37 | 38 | ;; Parsing YAML with unknown tags 39 | (yaml/parse-string "--- !foobar 40 | foo: HELLO WORLD") 41 | 42 | ;; This will parse properly 43 | (yaml/parse-string "--- !foobar 44 | foo: HELLO WORLD" :constructor yaml.reader/passthrough-constructor) 45 | 46 | ;; Dump YAML 47 | 48 | (yaml/generate-string {:foo "bar"}) 49 | 50 | ;; Examples 51 | 52 | (yaml/generate-string [{:name "John Smith", :age 33} {:name "Mary Smith", :age 27}]) 53 | ;; "- {name: John Smith, age: 33}\n- {name: Mary Smith, age: 27}\n" 54 | 55 | (yaml/parse-string " 56 | - {name: John Smith, age: 33} 57 | - name: Mary Smith 58 | age: 27 59 | ") 60 | 61 | => ({:name "John Smith", :age 33} 62 | {:name "Mary Smith", :age 27}) 63 | 64 | ;; Output Formatting examples 65 | 66 | (def data [{:name "John Smith", :age 33} {:name "Mary Smith", :age 27}]) 67 | 68 | (yaml/generate-string data) 69 | => - {age: 33, name: John Smith} 70 | - {age: 27, name: Mary Smith} 71 | 72 | (yaml/generate-string data :dumper-options {:flow-style :flow}) 73 | => [{age: 33, name: John Smith}, {age: 27, name: Mary Smith}] 74 | 75 | (yaml/generate-string data :dumper-options {:flow-style :block}) 76 | => - age: 33 77 | name: John Smith 78 | - age: 27 79 | name: Mary Smith 80 | 81 | (yaml/generate-string data :dumper-options {:flow-style :flow :scalar-style :single-quoted}) 82 | => [{'age': !!int '33', 'name': 'John Smith'}, {'age': !!int '27', 'name': 'Mary Smith'}] 83 | 84 | Valid values for flow-style are: 85 | - :auto 86 | - :block 87 | - :flow 88 | 89 | Valid values for scalar-style are: 90 | - :double-quoted 91 | - :single-quoted 92 | - :literal 93 | - :folded 94 | - :plain 95 | 96 | All are documented at http://yaml.org/spec/current.html 97 | ``` 98 | 99 | This is mainly an updated version of clj-yaml with some updates 100 | 101 | 1. Updates snake YAML to latest version 102 | 2. Split reader and writer into separate protocols and files 103 | 3. Ability to read YAML from file in single function 104 | 4. Return vector [] instead of list when parsing java.util.ArrayList 105 | 5. Ability to parse multiple documents 106 | 107 | ## License 108 | 109 | Distributed under the Eclipse Public License 110 | -------------------------------------------------------------------------------- /test/yaml/reader_test.clj: -------------------------------------------------------------------------------- 1 | (ns yaml.reader-test 2 | (:require [clojure.test :refer :all] 3 | [yaml.reader :refer :all]) 4 | (:import [org.yaml.snakeyaml.error YAMLException])) 5 | 6 | (def multiple-docs 7 | "foo\n---\nbar\n...") 8 | 9 | (def nested-hash-yaml 10 | "root:\n childa: a\n childb: \n grandchild: \n greatgrandchild: bar\n") 11 | 12 | (def list-yaml 13 | "--- # Favorite Movies\n- Casablanca\n- North by Northwest\n- The Man Who Wasn't There") 14 | 15 | (def hashes-lists-yaml " 16 | items: 17 | - part_no: A4786 18 | descrip: Water Bucket (Filled) 19 | price: 1.47 20 | quantity: 4 21 | - part_no: E1628 22 | descrip: High Heeled \"Ruby\" Slippers 23 | price: 100.27 24 | quantity: 1 25 | owners: 26 | - Dorthy 27 | - Wicked Witch of the East 28 | ") 29 | 30 | (def inline-list-yaml " 31 | --- # Shopping list 32 | [milk, pumpkin pie, eggs, juice] 33 | ") 34 | 35 | (def inline-hash-yaml 36 | "{name: John Smith, age: 33}") 37 | 38 | (def list-of-hashes-yaml " 39 | - {name: John Smith, age: 33} 40 | - name: Mary Smith 41 | age: 27 42 | ") 43 | 44 | (def hashes-of-lists-yaml " 45 | men: [John Smith, Bill Jones] 46 | women: 47 | - Mary Smith 48 | - Susan Williams 49 | ") 50 | 51 | (def typed-data-yaml " 52 | the-bin: !!binary 0101") 53 | 54 | (def set-yaml " 55 | --- !!set 56 | ? Mark McGwire 57 | ? Sammy Sosa 58 | ? Ken Griff") 59 | 60 | (def emojis-yaml 61 | ;; Unicode SMILING FACE WITH OPEN MOUTH AND SMILING EYES 62 | (apply str (Character/toChars 128516))) 63 | 64 | (def custom-tags-yaml 65 | "--- !ruby/hash:ActiveSupport::HashWithIndifferentAccess 66 | en: TEXT IN ENGLISH 67 | de: TEXT IN DEUTSCH 68 | list: !CustomList 69 | - foo 70 | - bar 71 | - baz 72 | number: !CustomScalar 1234 73 | string: !CustomScalar custom-string 74 | date: !Date custom-string 75 | expires_at: !ruby/object:DateTime 2017-05-09 05:27:43.000000000") 76 | 77 | (deftest parse-multiple-documents 78 | (testing "should handle multiple yaml documents" 79 | (is (= ["foo" "bar"] 80 | (parse-documents multiple-docs))))) 81 | 82 | (deftest parse-hash 83 | (let [parsed (parse-string "foo: bar")] 84 | (is (= "bar" (parsed :foo))))) 85 | 86 | (deftest parse-nested-hash 87 | (let [parsed (parse-string nested-hash-yaml)] 88 | (is (= "a" ((parsed :root) :childa))) 89 | (is (= "bar" ((((parsed :root) :childb) :grandchild) :greatgrandchild))))) 90 | 91 | (deftest parse-list 92 | (let [parsed (parse-string list-yaml)] 93 | (is (= "Casablanca" (first parsed))) 94 | (is (= "North by Northwest" (nth parsed 1))) 95 | (is (= "The Man Who Wasn't There" (nth parsed 2))))) 96 | 97 | (deftest parse-nested-hash-and-list 98 | (let [parsed (parse-string hashes-lists-yaml)] 99 | (is (= "A4786" ((first (parsed :items)) :part_no))) 100 | (is (= "Dorthy" (first ((nth (parsed :items) 1) :owners)))))) 101 | 102 | (deftest parse-inline-list 103 | (let [parsed (parse-string inline-list-yaml)] 104 | (is (= "milk" (first parsed))) 105 | (is (= "pumpkin pie" (nth parsed 1))) 106 | (is (= "eggs" (nth parsed 2))) 107 | (is (= "juice" (last parsed))))) 108 | 109 | (deftest parse-inline-hash 110 | (let [parsed (parse-string inline-hash-yaml)] 111 | (is (= "John Smith" (parsed :name))) 112 | (is (= 33 (parsed :age))))) 113 | 114 | (deftest parse-list-of-hashes 115 | (let [parsed (parse-string list-of-hashes-yaml)] 116 | (is (= "John Smith" ((first parsed) :name))) 117 | (is (= 33 ((first parsed) :age))) 118 | (is (= "Mary Smith" ((nth parsed 1) :name))) 119 | (is (= 27 ((nth parsed 1) :age))))) 120 | 121 | (deftest hashes-of-lists 122 | (let [parsed (parse-string hashes-of-lists-yaml)] 123 | (is (= "John Smith" (first (parsed :men)))) 124 | (is (= "Bill Jones" (last (parsed :men)))) 125 | (is (= "Mary Smith" (first (parsed :women)))) 126 | (is (= "Susan Williams" (last (parsed :women)))))) 127 | 128 | (deftest h-set 129 | (is (= #{"Mark McGwire" "Ken Griff" "Sammy Sosa"} 130 | (parse-string set-yaml)))) 131 | 132 | (deftest typed-data 133 | (let [parsed (parse-string typed-data-yaml)] 134 | (is (= (Class/forName "[B") (type (:the-bin parsed)))))) 135 | 136 | (deftest keywordized 137 | (is (= "items" (-> hashes-lists-yaml (parse-string :keywords false) ffirst))) 138 | (binding [*keywordize* false] 139 | (is (= "items" (-> hashes-lists-yaml parse-string ffirst)))) 140 | 141 | (testing "custom keywordize function" 142 | (binding [*keywordize* #(str % "-extra")] 143 | (let [obj (parse-string hashes-lists-yaml)] 144 | (is (= ["items-extra"] (keys obj))) 145 | (is (= ["part_no-extra" "descrip-extra" "price-extra" "quantity-extra"] 146 | (-> obj (get "items-extra") first keys))))))) 147 | 148 | (deftest emojis 149 | (is (pos? (count (parse-string emojis-yaml))))) 150 | 151 | (deftest unknown-tags 152 | (testing "with the regular old parser " 153 | (is (thrown-with-msg? YAMLException #"Invalid tag: !ruby/hash:ActiveSupport::HashWithIndifferentAccess" 154 | (parse-string custom-tags-yaml)))) 155 | (testing "with the passthrough-constructor" 156 | (is (= {:en "TEXT IN ENGLISH" 157 | :de "TEXT IN DEUTSCH" 158 | :list ["foo" "bar" "baz"] 159 | :number "1234" ;NOTE: tagged numbers are interpreted as strings 160 | :string "custom-string" 161 | :date "custom-string" 162 | :expires_at "2017-05-09 05:27:43.000000000"} 163 | (parse-string custom-tags-yaml :constructor passthrough-constructor))))) 164 | 165 | (deftest parse-string-thread-safety 166 | (testing "should not throw any exceptions when parsing objects concurrently" 167 | (let [results (doall (for [x (range 100)] 168 | (future (parse-string inline-list-yaml))))] 169 | (doseq [result results] 170 | ;; when non-threadsafe, Exceptions are thrown unexpectedly 171 | (is (= ["milk" "pumpkin pie" "eggs" "juice"] @result)))))) 172 | --------------------------------------------------------------------------------