 
   (fact "top level non-collections are canonicalized"
     (subject/any-required-seq->maps [:a :b]) => {[:a] [p/must-exist]
-                                                 [:b] [p/must-exist]})
-
-)
+                                                 [:b] [p/must-exist]}))
 
 ;;; Types can be expanded by signifier
-
 
 (facts expand-type-finders
   (subject/expand-type-finders {} []) => []




;; for a little clarity in the more complicated cases
(def type-has vector)


;; ..t.. is an irrelevant type-rep
(fact "various signifiers can be converted into a canonical form"
  (fact "signifiers that are maps"
    (fact "simple maps just have their keys and values vectorized"
      (subject/canonicalize ..t.. {:a p/must-exist}) => {[:a] [p/must-exist]}
      (subject/canonicalize ..t.. {:a p/must-exist, :b p/must-exist})
      => {[:a] [p/must-exist]
          [:b] [p/must-exist]})
    
    (fact "already vectorized keys and values are left alone"
      (subject/canonicalize ..t.. {[:a] p/must-exist}) => {[:a] [p/must-exist]}
      (subject/canonicalize ..t.. {[:a] [p/must-exist]}) => {[:a] [p/must-exist]}
      (subject/canonicalize ..t.. {[:a :b] p/must-exist}) => {[:a :b] [p/must-exist]})

    (fact "nested maps"
      (subject/canonicalize ..t.. {:a {:b p/must-exist}}) => {[:a :b] [p/must-exist]}))

  (fact "signifiers that are vectors"
    (fact "vectors are shorthand for maps with `p/must-exist`"
      (subject/canonicalize ..t.. [ :a :b ]) => {[:a] [p/must-exist] [:b] [p/must-exist]})
    (fact "alternate for above - for clarity"
      (subject/canonicalize ..t.. (type-has :a :b)) => {[:a] [p/must-exist] [:b] [p/must-exist]})
    (fact "single-element vectors are the same as atoms"
      (subject/canonicalize ..t.. (type-has [:a] [:b] ))
      => {[:a] [p/must-exist] [:b] [p/must-exist]})

    (fact "multi-element vectors are paths"
      (subject/canonicalize ..t.. (type-has [:a :c]
                                            [:b :d])) => {[:a :c] [p/must-exist]
                                                          [:b :d] [p/must-exist]})
    
    (fact "nested and unnested can be combined"
      (subject/canonicalize ..t.. (type-has :a [:b :c] :d)) => {[:a] [p/must-exist]
                                                                [:b :c] [p/must-exist]
                                                                [:d] [p/must-exist]})

    (fact "double-nesting indicates alternatives"
      (subject/canonicalize ..t.. (type-has :a [:b [1 2] :c] :d)) => {[:a] [p/must-exist]
                                                                      [:b 1 :c] [p/must-exist]
                                                                      [:b 2 :c] [p/must-exist]
                                                                      [:d] [p/must-exist]}

      (subject/canonicalize ..t.. (type-has [[:a :b]])) => {[:a] [p/must-exist]
                                                          [:b] [p/must-exist]})




    (fact "maps within vectors contribute paths - only paths - that must exist"
      (subject/canonicalize ..t.. [ {:a #'even?, :b #'even?} ]) => {[:a] [p/must-exist]
                                                                    [:b] [p/must-exist]}
      (subject/canonicalize ..t.. [ [:a {:b {:c #'even?}
                                         :d #'even?}] ]) => {[:a :b :c] [p/must-exist]
                                                             [:a :d] [p/must-exist]}

      (subject/canonicalize ..t.. [ [:a {:b #'even?} :e] ]) => (throws)))

  (fact "signifiers can only be maps and vectors"
    (subject/canonicalize ..t.. :a) => (throws #"maps or vectors"))

  (fact "multiple arguments are allowed"
    (subject/canonicalize ..t.. {:a p/must-exist} {:b p/must-exist}) => {[:a] [p/must-exist]
                                                                         [:b] [p/must-exist]}

    (fact "arguments with the same keys have their values merged"
      (subject/canonicalize ..t.. {:a p/must-exist} {:a #'even?}) => {[:a] [p/must-exist #'even?]}
      (subject/canonicalize ..t.. {:a {:b p/must-exist}}
                            {:a #'map?}
                            {:a {:b #'even?}}) => {[:a] [#'map?]
                                                   [:a :b] [p/must-exist #'even?]})

    (fact "maps and vectors can be merged"
      (subject/canonicalize ..t.. [ :a :b ] {:a #'even?}) => {[:a] [p/must-exist #'even?]
                                                              [:b] [p/must-exist]}))

  (fact "And here's an example of various ways to talk about a nested structure"
    ;; A figure has a color and many points
    (subject/canonicalize ..t.. [ :color
                           [:points ALL :x]
                           [:points ALL :y] ]) => {[:color] [p/must-exist]
                                                   [:points ALL :x] [p/must-exist]
                                                   [:points ALL :y] [p/must-exist]}

    (subject/canonicalize ..t.. [:color
                           [:points ALL [:x :y]]]) => {[:color] [p/must-exist]
                                                       [:points ALL :x] [p/must-exist]
                                                       [:points ALL :y] [p/must-exist]}


    (fact "maps can contribute to the `required` list"
      (let [point {[:x] [#'integer?]
                   [:y] [#'integer?]}]
        ;; Here we require x and y to exist, but lose the requirement they be integers.
        (subject/canonicalize ..t.. [:color [:points ALL point]])
        => {[:color] [p/must-exist]
            [:points ALL :x] [p/must-exist]
            [:points ALL :y] [p/must-exist]}
        
        ;; To get those options back, we use a map.
        (subject/canonicalize ..t.. [:color [:points ALL point]]
                              {:color #'string?
                               [:points ALL] point})
        => {[:color] [p/must-exist #'string?]
            [:points ALL :x] [p/must-exist #'integer?]
            [:points ALL :y] [p/must-exist #'integer?]})


      ;; If a nested type has required keys, those are preserved.
      (let [required-xy-point {[:x] [p/must-exist #'integer?]
                               [:y] [p/must-exist #'integer?]}]
        ;; As above, a mention in the required list transfers required-ness
        (subject/canonicalize ..t.. [:color [:points ALL required-xy-point]])
        => {[:color] [p/must-exist]
            [:points ALL :x] [p/must-exist]
            [:points ALL :y] [p/must-exist]}
        
        ;; To get those options back, we use a map.
        (subject/canonicalize ..t.. [:color]
                              {:color #'string?
                               [:points ALL] required-xy-point})
        => {[:color] [p/must-exist #'string?]
            [:points ALL :x] [p/must-exist #'integer?]
            [:points ALL :y] [p/must-exist #'integer?]})))


  (fact "this is a pretty unlikely map to use, but it will work"
    (subject/canonicalize ..t..
       {:points {ALL {:x #'integer?
                      :y #'integer?}}}) => {[:points ALL :x] [#'integer?]
                                            [:points ALL :y] [#'integer?]})
  )





;; ;; Rework into canonicalize ..t..
;;    (subject/expand-path-shorthand :x) => [:x]

      ;; (subject/expand-path-shorthand {:a #'even? :b #'even?})
      ;; =future=> (just [:a] [:b] :in-any-order)
      ;; (subject/expand-path-shorthand {:a {:b #'even?} :c #'even?})
      ;; =future=> (just [:a :b] [:c] :in-any-order)
;; (future-fact "three-argument form allows lookup in a type-map"
;;     (let [type-map {:Point {[:x] [p/must-exist #'integer?]
;;                             [:y] [p/must-exist #'integer?]}}]

            [structural-typing.predicates :as p])
  (:require [com.rpl.specter :refer [ALL]])
  (:use midje.sweet))

;;; The core

;; for a little clarity in the more complicated cases
(def type-has vector)


;; ..t.. is an irrelevant type-rep
(fact "type descriptions that are maps"
  (fact "simple maps just have their keys and values vectorized"
    (subject/canonicalize ..t.. {:a p/must-exist}) => {[:a] [p/must-exist]}
    (subject/canonicalize ..t.. {:a p/must-exist, :b p/must-exist})
    => {[:a] [p/must-exist]
        [:b] [p/must-exist]})
  
  (fact "already vectorized keys and values are left alone"
    (subject/canonicalize ..t.. {[:a] p/must-exist}) => {[:a] [p/must-exist]}
    (subject/canonicalize ..t.. {[:a] [p/must-exist]}) => {[:a] [p/must-exist]}
    (subject/canonicalize ..t.. {[:a :b] p/must-exist}) => {[:a :b] [p/must-exist]})

  (fact "nested maps have keys flattened into paths"
    (subject/canonicalize ..t.. {:a {:b p/must-exist}}) => {[:a :b] [p/must-exist]}))

(fact "type descriptions that are vectors"
  (fact "vectors are shorthand for maps with `p/must-exist`"
    (subject/canonicalize ..t.. [ :a :b ]) => {[:a] [p/must-exist] [:b] [p/must-exist]})
  (fact "alternate for above - for clarity"
    (subject/canonicalize ..t.. (type-has :a :b)) => {[:a] [p/must-exist] [:b] [p/must-exist]})
  (fact "single-element vectors are the same as atoms"
    (subject/canonicalize ..t.. (type-has [:a] [:b] ))
    => {[:a] [p/must-exist] [:b] [p/must-exist]})

  (fact "multi-element vectors are paths"
    (subject/canonicalize ..t.. (type-has [:a :c]
                                          [:b :d])) => {[:a :c] [p/must-exist]
                                                        [:b :d] [p/must-exist]})
  
  (fact "nested and unnested elements can be combined"
    (subject/canonicalize ..t.. (type-has :a [:b :c] :d)) => {[:a] [p/must-exist]
                                                              [:b :c] [p/must-exist]
                                                              [:d] [p/must-exist]})

  (fact "double-nesting indicates alternatives"
    (subject/canonicalize ..t.. (type-has :a [:b [1 2] :c] :d)) => {[:a] [p/must-exist]
                                                                    [:b 1 :c] [p/must-exist]
                                                                    [:b 2 :c] [p/must-exist]
                                                                    [:d] [p/must-exist]}

    (subject/canonicalize ..t.. (type-has [[:a :b]])) => {[:a] [p/must-exist]
                                                          [:b] [p/must-exist]})


  (fact "maps within vectors contribute paths - only paths - that must exist"
    (subject/canonicalize ..t.. [ {:a #'even?, :b #'even?} ]) => {[:a] [p/must-exist]
                                                                  [:b] [p/must-exist]}
    (subject/canonicalize ..t.. [ [:a {:b {:c #'even?}
                                       :d #'even?}] ]) => {[:a :b :c] [p/must-exist]
                                                           [:a :d] [p/must-exist]}

      (subject/canonicalize ..t.. [ [:a {:b #'even?} :e] ]) => (throws)))

(fact "signifiers can only be maps and vectors"
  (subject/canonicalize ..t.. :a) => (throws #"maps or vectors"))

(fact "multiple arguments are allowed"
  (subject/canonicalize ..t.. {:a p/must-exist} {:b p/must-exist}) => {[:a] [p/must-exist]
                                                                       [:b] [p/must-exist]}

  (fact "arguments with the same keys have their values merged"
    (subject/canonicalize ..t.. {:a p/must-exist} {:a #'even?}) => {[:a] [p/must-exist #'even?]}
    (subject/canonicalize ..t.. {:a {:b p/must-exist}}
                          {:a #'map?}
                          {:a {:b #'even?}}) => {[:a] [#'map?]
                                                 [:a :b] [p/must-exist #'even?]})

  (fact "maps and vectors can be merged"
    (subject/canonicalize ..t.. [ :a :b ] {:a #'even?}) => {[:a] [p/must-exist #'even?]
                                                            [:b] [p/must-exist]}))

(fact "And here's an example of various ways to talk about a nested structure"
  ;; A figure has a color and many points
  (subject/canonicalize ..t.. [ :color
                               [:points ALL :x]
                               [:points ALL :y] ]) => {[:color] [p/must-exist]
                                                       [:points ALL :x] [p/must-exist]
                                                       [:points ALL :y] [p/must-exist]}
                               
  (subject/canonicalize ..t.. [:color
                               [:points ALL [:x :y]]]) => {[:color] [p/must-exist]
                                                           [:points ALL :x] [p/must-exist]
                                                           [:points ALL :y] [p/must-exist]}

  (fact "maps can contribute to the `required` list"
    (let [point {[:x] [#'integer?]
                 [:y] [#'integer?]}]
      ;; Here we require x and y to exist, but lose the requirement they be integers.
      (subject/canonicalize ..t.. [:color [:points ALL point]])
      => {[:color] [p/must-exist]
          [:points ALL :x] [p/must-exist]
          [:points ALL :y] [p/must-exist]}
        
      ;; To get those options back, we use a map.
      (subject/canonicalize ..t.. [:color [:points ALL point]]
                            {:color #'string?
                             [:points ALL] point})
      => {[:color] [p/must-exist #'string?]
          [:points ALL :x] [p/must-exist #'integer?]
          [:points ALL :y] [p/must-exist #'integer?]})


    ;; If a nested type has required keys, those are preserved.
    (let [required-xy-point {[:x] [p/must-exist #'integer?]
                             [:y] [p/must-exist #'integer?]}]
      ;; As above, a mention in the required list transfers required-ness
      (subject/canonicalize ..t.. [:color [:points ALL required-xy-point]])
      => {[:color] [p/must-exist]
          [:points ALL :x] [p/must-exist]
          [:points ALL :y] [p/must-exist]}
      
      ;; To get those options back, we use a map.
      (subject/canonicalize ..t.. [:color]
                            {:color #'string?
                             [:points ALL] required-xy-point})
      => {[:color] [p/must-exist #'string?]
          [:points ALL :x] [p/must-exist #'integer?]
          [:points ALL :y] [p/must-exist #'integer?]})))

(fact "this is a pretty unlikely map to use, but it will work"
  (subject/canonicalize ..t..
                        {:points {ALL {:x #'integer?
                                       :y #'integer?}}}) => {[:points ALL :x] [#'integer?]
                                                             [:points ALL :y] [#'integer?]})





;;;; Utilities


(facts "nested-map->path-map flattens nested keys into a vector of keys"
