module Main exposing (main) -- import Html.Lazy as HLazy import Browser import Dict import Html as H exposing (Attribute, Html) import Html.Attributes as A import Html.Events as Ev import Http import Json.Decode as J exposing (Decoder) import Json.Encode as Encode import OrderedDict exposing (OrderedDict) main : Program () Model Msg main = Browser.element { init = init , update = update , subscriptions = \_ -> Sub.none , view = view } type alias ArgType = { kind : String , name : Maybe String , ofType : OfArgType } type alias FieldType = ArgType type OfArgType = OfArgType (Maybe ArgType) type alias Arg = { defaultValue : J.Value , description : Maybe String , name : String , argType : ArgType } type alias Directive = { args : List Arg , locations : Maybe (List String) , description : Maybe String , name : String } type alias Field = { name : String , description : Maybe String , args : List Arg , fieldType : FieldType , isDeprecated : Bool , deprecationReason : Maybe String } type alias Interface = J.Value type alias EnumValue = J.Value type alias PossibleType = ArgType type alias QLType = { kind : String , name : String , description : Maybe String , fields : Maybe (List Field) , inputFields : Maybe (List Field) , interfaces : Maybe (List Interface) , enumValues : Maybe (List EnumValue) , possibleTypes : Maybe (List PossibleType) } type alias Introspection = { directives : List Directive , mutationType : Maybe String , queryType : Maybe String , subscriptionType : Maybe String , types : List QLType } type IntrospectionResult = Loading | Error Http.Error | Success Introspection type alias Model = { introspectUrl : String , introspections : OrderedDict String IntrospectionResult , collapsedIntrospections : OrderedDict String Bool } init : () -> ( Model, Cmd Msg ) init _ = ( Model "https://www.graphqlhub.com/graphql" OrderedDict.empty OrderedDict.empty, Cmd.none ) type Msg = UpdateIntrospectUrl String | RequestIntrospection | IntrospectionRequest String (Result Http.Error Introspection) | CollapseIntrospection String | UncollapseIntrospection String update : Msg -> Model -> ( Model, Cmd Msg ) update msg model = case msg of CollapseIntrospection url -> ( { model | collapsedIntrospections = OrderedDict.insert url True model.collapsedIntrospections }, Cmd.none ) UncollapseIntrospection url -> ( { model | collapsedIntrospections = OrderedDict.insert url False model.collapsedIntrospections }, Cmd.none ) UpdateIntrospectUrl url -> ( { model | introspectUrl = url }, Cmd.none ) RequestIntrospection -> ( { model | introspections = OrderedDict.insert model.introspectUrl Loading model.introspections }, requestIntrospection model.introspectUrl ) IntrospectionRequest url result -> let val = case result of Ok i -> Success i Err e -> Error e in ( { model | introspections = OrderedDict.insert url val model.introspections }, Cmd.none ) requestIntrospection : String -> Cmd Msg requestIntrospection url = Http.post { url = url , body = introspectionQuery , expect = Http.expectJson (IntrospectionRequest url) decodeIntrospection } view : Model -> Html Msg view model = H.div [ A.id "app" ] [ header model , H.main_ [] [ H.ul [] (List.filterMap (\u -> introspectionResultView (Maybe.withDefault False (Dict.get u model.collapsedIntrospections.dict)) u (Dict.get u model.introspections.dict) ) model.introspections.order ) ] ] header : Model -> Html Msg header model = H.header (styleGroup [ ( "display", "flex" ), ( "align-items", "center" ) ]) [ H.h1 (styleGroup [ ( "font-size", "inherit" ) , ( "padding", "0.5em" ) , ( "width", "100%" ) , ( "white-space", "nowrap" ) , ( "flex", "1" ) ] ) [ H.text "GraphQL Introspector" ] , H.form (Ev.onSubmit RequestIntrospection :: styleGroup [ ( "width", "100%" ) , ( "display", "flex" ) , ( "align-items", "center" ) ] ) [ H.input (styleGroup [ ( "width", "100%" ) , ( "padding", "0.5em" ) , ( "margin", "0.25em 0 0.25em 0" ) ] ++ [ A.value model.introspectUrl , Ev.onInput UpdateIntrospectUrl , A.placeholder "https://your-graphql-endpoint.example.com/graphql" ] ) [] , H.button (Ev.onClick RequestIntrospection :: styleGroup [ ( "padding", "0.5em" ) , ( "margin", "0.25em" ) ] ) [ H.text "Add" ] ] ] introspectionResultView : Bool -> String -> Maybe IntrospectionResult -> Maybe (Html Msg) introspectionResultView is_hidden url mir = Maybe.map (\ir -> H.li (styleGroup [ ( "background-color", lighten ) , ( "padding", "0.5em" ) , ( "border-radius", "0.5em" ) , ( "margin", "0 0.25em 0.25em 0.25em" ) ] ) (H.header (styleGroup [ ( "margin-bottom", "0.5em" ), ( "border-bottom", "solid 1px #888" ), ( "padding-bottom", "0.75em" ) ]) [ H.text "" , H.a [ A.href url , A.target "_blank" , A.title (Debug.toString mir) ] [ H.text url ] , H.span [ Ev.onClick ((if is_hidden then UncollapseIntrospection else CollapseIntrospection) url) ] [ H.text (if is_hidden then "Show" else "Hide") ] ] :: (case ir of Loading -> [ H.text "Loading..." ] Error e -> [ H.text ("Error: " ++ httpErrorToString e) ] Success i -> if not is_hidden then [ introspectionView i ] else [ collapsedIntrospectionView i url ] ) ) ) mir collapsedIntrospectionView : Introspection -> String -> Html Msg collapsedIntrospectionView i url = H.div [ Ev.onClick (UncollapseIntrospection url) ] [ H.text "Expand" ] introspectionView : Introspection -> Html Msg introspectionView i = let types = keyBy .name i.types queryTypes = List.filter (\t -> Just t.name == i.queryType) i.types mutationTypes = List.filter (\t -> Just t.name == i.mutationType) i.types allTypes = List.filter (\t -> not (List.member t.name <| List.filterMap identity [ i.mutationType, i.queryType ] ) && not (String.startsWith "__" t.kind) ) i.types sg = styleGroup [ ( "margin", "1em 0" ) ] rootQueryTypes = Debug.log "i.queryType" ( i.queryType |> Maybe.andThen (\a -> Dict.get a types.dict) |> Maybe.andThen .fields |> Maybe.map (List.map fieldView) |> Maybe.withDefault [] ) in H.div [] [ H.div sg [ H.h2 [] [ H.text ("Query Types (" ++ (Maybe.withDefault "N/A" i.queryType) ++ ")") ] , H.div (styleGroup [ ( "margin-left", "2em" ) ]) rootQueryTypes ] , H.div sg [ H.h2 [] [ H.text "Query Fields" ] , H.div [] (List.map typeView queryTypes) ] , H.div sg [ H.h2 [] [ H.text "Mutations" ] , H.div [] (List.map typeView mutationTypes) ] , H.div sg [ H.h2 [] [ H.text "All Types" ] , H.div [] (List.map typeView allTypes) ] ] kindColor : String -> String kindColor s = case s of "OBJECT" -> "#0af" "SCALAR" -> "#fa0" "ENUM" -> "#f0a" "INTERFACE" -> "#af0" "UNION" -> "#0fa" "INPUT_OBJECT" -> "#a0f" _ -> "#fff" typeView : QLType -> Html Msg typeView qt = H.div [] ([ H.div (styleGroup [ ( "display", "flex" ) , ( "align-items", "center" ) , ( "font-size", "16px" ) , ( "margin", "0.25em 0" ) ] ) [ H.code (styleGroup [ ( "margin-right", "0.5em" ) , ( "color", "#111" ) , ( "font-size", "75%" ) , ( "display", "inline-block" ) , ( "padding", "0.25em" ) , ( "border-radius", "0.25em" ) , ( "line-height", "1" ) , ( "background-color", kindColor qt.kind ) ] ) [ H.text qt.kind ] , H.code [ A.title (Debug.toString qt) ] [ H.text qt.name ] ] ] ++ (case qt.description of Just s -> [ H.p (styleGroup [ ( "margin-left", "2em" ) ]) [ H.text s ] ] Nothing -> [] ) ++ (case qt.fields of Just fields -> [ H.div (styleGroup [ ( "margin-left", "4em" ) ]) <| List.map fieldView fields ] Nothing -> [] ) ) fieldView : Field -> Html Msg fieldView f = H.li [ A.title <| Debug.toString f ] [ H.text f.name ] httpErrorToString : Http.Error -> String httpErrorToString e = -- TODO: refactor as expect function case e of Http.BadUrl s -> "Bad URL: " ++ s Http.Timeout -> "Timeout" Http.NetworkError -> "Network Error" Http.BadStatus n -> "Bad Status: " ++ String.fromInt n Http.BadBody s -> "Bad Body: " ++ s decodeIntrospection : Decoder Introspection decodeIntrospection = J.field "data" (J.field "__schema" (J.map5 Introspection (J.field "directives" (J.list decodeDirective)) (J.maybe (J.field "mutationType" (J.field "name" J.string))) (J.maybe (J.field "queryType" (J.field "name" J.string))) (J.maybe (J.field "subscriptionType" (J.field "name" J.string))) (J.field "types" (J.list decodeType)) ) ) decodeType : Decoder QLType decodeType = J.map8 QLType (J.field "kind" J.string) (J.field "name" J.string) (J.maybe (J.field "description" J.string)) (J.maybe (J.field "fields" (J.list decodeField))) (J.maybe (J.field "inputFields" (J.list decodeField))) (J.maybe (J.field "interfaces" (J.list decodeInterface))) (J.maybe (J.field "enumValues" (J.list decodeEnumValue))) (J.maybe (J.field "possibleTypes" (J.list decodePossibleType))) decodeField : Decoder Field decodeField = J.map6 Field (J.field "name" J.string) (J.maybe (J.field "description" J.string)) (J.field "args" (J.list decodeArg)) (J.field "type" decodeFieldType) (J.field "isDeprecated" J.bool) (J.maybe (J.field "deprecationReason" J.string)) decodePossibleType : Decoder PossibleType decodePossibleType = decodeArgType decodeEnumValue : Decoder J.Value decodeEnumValue = J.value decodeInterface : Decoder J.Value decodeInterface = J.value decodeDirective : Decoder Directive decodeDirective = J.map4 Directive (J.field "args" (J.list decodeArg)) (J.maybe (J.field "locations" (J.list J.string))) (J.maybe (J.field "description" J.string)) (J.field "name" J.string) decodeArg : Decoder Arg decodeArg = J.map4 Arg (J.field "defaultValue" J.value) (J.maybe (J.field "description" J.string)) (J.field "name" J.string) (J.field "type" decodeArgType) decodeFieldType : Decoder ArgType decodeFieldType = J.map3 ArgType (J.field "kind" J.string) (J.maybe (J.field "name" J.string)) (J.field "ofType" decodeLazyArgType) decodeArgType : Decoder ArgType decodeArgType = J.map3 ArgType (J.field "kind" J.string) (J.maybe (J.field "name" J.string)) (J.field "ofType" decodeLazyArgType) decodeLazyArgType : Decoder OfArgType decodeLazyArgType = J.map OfArgType (J.maybe (J.lazy (\_ -> decodeArgType))) introspectionQuery : Http.Body introspectionQuery = -- this query was copied from the one graphiql made Http.jsonBody (Encode.object [ ( "query", Encode.string "query IntrospectionQuery { __schema { queryType { name } mutationType { name } subscriptionType { name } types { ...FullType } directives { name description args { ...InputValue }}}} fragment FullType on __Type { kind name description fields(includeDeprecated: true) { name description args { ...InputValue } type { ...TypeRef } isDeprecated deprecationReason } inputFields { ...InputValue } interfaces { ...TypeRef } enumValues(includeDeprecated: true) { name description isDeprecated deprecationReason } possibleTypes { ...TypeRef }} fragment InputValue on __InputValue { name description type { ...TypeRef } defaultValue} fragment TypeRef on __Type { kind name ofType { kind name ofType { kind name ofType { kind name ofType { kind name ofType { kind name ofType { kind name ofType { kind name }}}}}}}}" ) ]) styleGroup : List ( String, String ) -> List (Attribute Msg) styleGroup l = List.map (\( k, v ) -> A.style k v) l lighten : String lighten = "rgba(255, 255, 255, 0.05)" keyBy : (a -> comparable) -> List a -> OrderedDict comparable a keyBy f l = List.foldl (\t -> \d -> OrderedDict.insert (f t) t d) OrderedDict.empty l getByKey : comparable -> OrderedDict comparable v -> Maybe v getByKey k d = Dict.get k d.dict