| |
| ( ns metabase.util.malli.doc
( :require
[ clojure.java.io :as io ]
[ hiccup.core :as hiccup ]
[ malli.core :as mc ]
[ malli.error :as me ]
[ metabase.util.log :as log ]
[ metabase.util.markdown :as markdown ] )
( :import
( org.apache.commons.io FileUtils ) ) )
|
|
| ( set! *warn-on-reflection* true )
|
|
| ( defmulti ^ :private generate-dox-method
{ :arglists ' ( [ schema cache ] ) }
( fn [ schema _cache ]
{ :pre [ ( instance? malli.core.Schema schema ) ] }
( mc/-type ( mc/-parent schema ) ) ) )
|
|
| ( defn- maybe-parse-markdown [ x ]
( cond-> x
( string? x ) ( markdown/process-markdown :html ) ) )
|
|
| ( defn- explicit-dox [ schema ]
{ :pre [ ( instance? malli.core.Schema schema ) ] }
( or ( when-let [ x ( ( some-fn :description :error/message )
( mc/properties schema ) ) ]
( maybe-parse-markdown x ) )
( when ( symbol? ( mc/form schema ) )
( when-let [ msg ( me/error-message { :schema schema , :unknown false } ) ]
( when-not ( = msg "unknown error" )
msg ) ) ) ) )
|
|
| ( defn- generate-dox
( [ schema ]
( generate-dox schema { } ) )
( [ schema cache ]
( let [ schema ( if ( instance? malli.core.Schema schema )
schema
( mc/schema schema ) )
schema ( or ( some-> ( :doc/schema ( mc/-properties schema ) ) mc/schema )
schema )
explicit-dox ( explicit-dox schema )
{ generated :this , cache :cache } ( generate-dox-method schema cache )
_ ( assert ( some? generated ) )
_ ( assert ( map? cache ) )
dox ( if explicit-dox
[ :div
[ :div explicit-dox ]
[ :div generated ] ]
generated ) ]
{ :this dox , :cache cache } ) ) )
|
|
| ( defmethod generate-dox-method :default
[ schema cache ]
{ :this ( if ( explicit-dox schema )
[ :div ]
( or
( me/error-message { :schema schema , :unknown false } )
( do
( log/warnf "WARNING: UNKNOWN SCHEMA: %s" ( pr-str schema ) )
[ :div
{ :style "border: 2px solid red;" }
[ :div
[ :b "UNKNOWN SCHEMA: " ]
[ :tt ( mc/-type ( mc/-parent schema ) ) ] ]
[ :div [ :pre ( pr-str schema ) ] ]
[ :div [ :pre ( mc/properties schema ) ] ] ] ) ) )
:cache cache } )
|
|
| ( defmethod generate-dox-method :ref
[ schema cache ]
( let [ [ child ] ( mc/children schema ) ]
( generate-dox child cache ) ) )
|
|
| ( defmethod generate-dox-method :schema
[ schema cache ]
( let [ [ child ] ( mc/children schema ) ]
( generate-dox child cache ) ) )
|
|
| ( defn- keyword-schema-relative-file-name [ k ]
{ :pre [ ( qualified-keyword? k ) ] }
( format "%s__%s.html"
( munge ( namespace k ) )
( munge ( name k ) ) ) )
|
|
| ( defn- schema-title [ schema ]
( or ( some-> ( :doc/title ( mc/properties schema ) ) maybe-parse-markdown )
( when-not ( instance? malli.core.Schema schema )
( schema-title ( mc/schema schema ) ) )
( when ( mc/-ref-schema? schema )
( schema-title ( mc/deref schema ) ) )
( when ( keyword? schema )
[ :pre ( name schema ) ] ) ) )
|
|
| ( defn- ^ :dynamic *keyword-schema-link* [ k ]
[ :a
{ :href ( keyword-schema-relative-file-name k ) }
( schema-title k ) ] )
|
|
keyword schema
| ( defmethod generate-dox-method :malli.core/schema
[ schema cache ]
( let [ k ( mc/-form schema ) ]
( assert ( keyword? k ) )
( cond
( and ( qualified-keyword? k )
( contains? cache k ) )
{ :this ( *keyword-schema-link* k )
:cache cache }
( qualified-keyword? k )
( let [ cache ( assoc cache k nil )
{ :keys [ this cache ] } ( generate-dox ( mc/deref ( mc/schema k ) ) cache ) ]
{ :this ( *keyword-schema-link* k )
:cache ( assoc cache k this ) } )
:else
( ( get-method generate-dox-method :default ) schema cache ) ) ) )
|
|
For things that have n children, like :and or `:or.
| ( defn- generate-dox-for-schemas
[ schemas cache ]
( reduce
( fn [ { :keys [ cache these ] } schema ]
( let [ { :keys [ this cache ] } ( generate-dox schema cache ) ]
{ :cache cache
:these ( conj these this ) } ) )
{ :cache cache , :these [ ] }
schemas ) )
|
|
| ( defmethod generate-dox-method :and
[ schema cache ]
( let [ { :keys [ cache these ] } ( generate-dox-for-schemas ( mc/children schema ) cache ) ]
{ :this [ :div
"Must satisfy all of these:"
( into [ :ul ]
( map ( fn [ child ]
[ :li child ] ) )
these ) ]
:cache cache } ) )
|
|
| ( defmethod generate-dox-method :or
[ schema cache ]
( let [ { :keys [ cache these ] } ( generate-dox-for-schemas ( mc/children schema ) cache ) ]
{ :this [ :div
"Must be one of these:"
( into [ :ul ]
( map ( fn [ child ]
[ :li child ] ) )
these ) ]
:cache cache } ) )
|
|
| ( defmethod generate-dox-method :enum
[ schema cache ]
{ :this [ :div
"Must be equal to one of these:"
( into [ :ul ]
( map ( fn [ child ]
[ :li [ :pre ( pr-str child ) ] ] ) )
( mc/children schema ) ) ]
:cache cache } )
|
|
| ( defn- generate-table-rows-for-keyed-children [ children cache ]
( reduce
( fn [ { :keys [ cache rows ] } [ k opts child ] ]
( let [ { :keys [ cache this ] } ( generate-dox child cache )
row [ :tr
{ :style "border: 1px solid black;" }
[ :td
{ :style "border: 1px solid black; background: #cccccc;" }
[ :pre k ]
( when ( :optional opts )
[ :i "Optional." ] ) ]
[ :td
{ :style "border: 1px solid black;" }
[ :div ( maybe-parse-markdown ( :description opts ) ) ]
[ :div this ] ] ] ]
{ :cache cache , :rows ( conj rows row ) } ) )
{ :cache cache , :rows [ ] }
children ) )
|
|
| ( defmethod generate-dox-method :map
[ schema cache ]
( if-let [ children ( not-empty ( mc/children schema ) ) ]
( let [ { :keys [ rows cache ] } ( generate-table-rows-for-keyed-children children cache ) ]
{ :this [ :div
"A map with the following keys:"
[ :table
[ :thead
[ :tr [ :th "Key" ] [ :th "Schema" ] ] ]
( into [ :tbody ] rows ) ] ]
:cache cache } )
{ :this "A map"
:cache cache } ) )
|
|
| ( defmethod generate-dox-method :merge
[ schema cache ]
( generate-dox ( mc/deref schema ) cache ) )
|
|
| ( defmethod generate-dox-method :maybe
[ schema cache ]
( let [ [ child ] ( mc/children schema )
{ :keys [ this cache ] } ( generate-dox child cache ) ]
{ :this [ :div
"Either " [ :code "nil" ] ", or "
[ :div this ] ]
:cache cache } ) )
|
|
| ( defmethod generate-dox-method :sequential
[ schema cache ]
( let [ [ child ] ( mc/children schema )
{ :keys [ this cache ] } ( generate-dox child cache ) ]
{ :this [ :div
[ :div "A sequence of:" ]
[ :div this ] ]
:cache cache } ) )
|
|
| ( defmethod generate-dox-method :map-of
[ schema cache ]
( let [ [ k-schema v-schema ] ( mc/children schema )
{ k-dox :this , :keys [ cache ] } ( generate-dox k-schema cache )
{ v-dox :this , :keys [ cache ] } ( generate-dox v-schema cache ) ]
{ :this [ :div
"Must be a map:"
[ :ul
[ :li
[ :div
"With keys satisfying:"
[ :div k-dox ] ] ]
[ :li
[ :div
"With values satisfying:"
[ :div v-dox ] ] ] ] ]
:cache cache } ) )
|
|
| ( defmethod generate-dox-method :keyword
[ _schema cache ]
{ :this "Must be a keyword."
:cache cache } )
|
|
| ( defmethod generate-dox-method :catn
[ schema cache ]
( let [ { :keys [ rows cache ] } ( generate-table-rows-for-keyed-children ( mc/children schema ) cache ) ]
{ :this [ :div
"A sequence with the shape"
[ :table
( into [ :tbody ] rows ) ] ]
:cache cache } ) )
|
|
| ( defmethod generate-dox-method :cat
[ schema cache ]
( let [ { :keys [ these cache ] } ( generate-dox-for-schemas ( mc/children schema ) cache ) ]
{ :this [ :div
"Sequence with the shape"
( into [ :ul ]
( map ( fn [ item ]
[ :li item ] ) )
these ) ]
:cache cache } ) )
|
|
| ( defmethod generate-dox-method :?
[ schema cache ]
( let [ [ child ] ( mc/children schema )
{ :keys [ this cache ] } ( generate-dox child cache ) ]
{ :this [ :div
"Zero or one instances of"
[ :div this ] ]
:cache cache } ) )
|
|
| ( defmethod generate-dox-method :*
[ schema cache ]
( let [ [ child ] ( mc/children schema )
{ :keys [ this cache ] } ( generate-dox child cache ) ]
{ :this [ :div
"Zero or more instances of"
[ :div this ] ]
:cache cache } ) )
|
|
| ( defmethod generate-dox-method :+
[ schema cache ]
( let [ [ child ] ( mc/children schema )
{ :keys [ this cache ] } ( generate-dox child cache ) ]
{ :this [ :div
"One or more instances of"
[ :div this ] ]
:cache cache } ) )
|
|
| ( defmethod generate-dox-method :=
[ schema cache ]
( let [ [ child ] ( mc/children schema ) ]
{ :this [ :div "Must equal " [ :code ( pr-str child ) ] ]
:cache cache } ) )
|
|
| ( defmethod generate-dox-method :any
[ _schema cache ]
{ :this "anything"
:cache cache } )
|
|
we'll assume that all the possible dispatch values are possible, and treat this like an :or .
| ( defmethod generate-dox-method :multi
[ schema cache ]
( let [ { :keys [ rows cache ] } ( generate-table-rows-for-keyed-children ( mc/children schema ) cache ) ]
{ :this [ :div
"One of the following types of expressions:"
[ :table
[ :thead
[ :tr [ :th "Type" ] [ :th "Schema" ] ] ]
( into [ :tbody ] rows ) ] ]
:cache cache } ) )
|
|
| ( defmethod generate-dox-method :boolean
[ _schema cache ]
{ :this [ :span "must be either " [ :code "true" ] " or " [ :code "false" ] "." ]
:cache cache } )
|
|
| ( defn- html [ title content ]
( hiccup/html
[ :html
[ :head ]
[ :body
{ :style "margin: 0;" }
[ :h1 title ]
content ] ] ) )
|
|
| ( defn- clean-target-dir! [ target-dir ]
( log/infof "CLEAN %s" target-dir )
( let [ target-dir ( io/file target-dir ) ]
( when ( .exists target-dir )
( assert ( .isDirectory target-dir ) )
( FileUtils/deleteDirectory target-dir ) )
( .mkdirs target-dir ) ) )
|
|
| ( defn- index-html-body [ cache ]
[ :div
( into [ :ul ]
( map ( fn [ keyword-schema ]
[ :li [ :div ( *keyword-schema-link* keyword-schema ) ] ] ) )
( sort ( keys cache ) ) ) ] )
|
|
| ( defn- write-index-html! [ target-dir cache ]
( let [ filename ( str target-dir "/index.html" ) ]
( log/infof "WRITE %s" filename )
( spit filename ( html "Index" ( index-html-body cache ) ) ) ) )
|
|
| ( defn- write-doc-pages! [ target-dir cache ]
( doseq [ [ keyword-schema content ] cache
:let [ filename ( str target-dir "/" ( keyword-schema-relative-file-name keyword-schema ) ) ] ]
( log/infof "WRITE %s" filename )
( spit filename ( html ( schema-title keyword-schema ) content ) ) ) )
|
|
| ( defn- generate-documentation! [ schema target-dir ]
( clean-target-dir! target-dir )
( let [ { :keys [ cache ] } ( generate-dox schema ) ]
( write-index-html! target-dir cache )
( write-doc-pages! target-dir cache ) )
( log/info "DONE." ) )
|
|
e.g.
clj -X metabase.util.malli.doc/generate-legacy-mbql-dox
clj -X metabase.util.malli.doc/generate-legacy-mbql-dox :target-dir '"target/docs/schemas/legacy-mbql"'
| ( defn generate-legacy-mbql-dox
[ { :keys [ target-dir ]
:or { target-dir "docs/legacy-mbql" } } ]
( require ' metabase.legacy-mbql.schema )
( generate-documentation! :metabase.legacy-mbql.schema/Query
target-dir ) )
|
|
e.g.
clj -X metabase.util.malli.doc/generate-pmbql-dox
clj -X metabase.util.malli.doc/generate-pmbql-dox :target-dir '"target/docs/schemas/pmbql"'
| ( defn generate-pmbql-dox
[ { :keys [ target-dir ]
:or { target-dir "target/docs/schemas/pmbql" } } ]
( require ' metabase.lib.schema )
( generate-documentation! :metabase.lib.schema/query
target-dir ) )
|
|
| |