module Main exposing (main) 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) import Url 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 } init : () -> ( Model, Cmd Msg ) init _ = ( Model "https://www.graphqlhub.com/graphql" OrderedDict.empty, Cmd.none ) type Msg = UpdateIntrospectUrl String | RequestIntrospection | IntrospectionRequest String (Result Http.Error Introspection) update : Msg -> Model -> ( Model, Cmd Msg ) update msg model = case msg of 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 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.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 : String -> Maybe IntrospectionResult -> Maybe (Html Msg) introspectionResultView url mir = case mir of Just ir -> Just (H.li [] ([ H.text url ] ++ (case ir of Loading -> [ H.text "Loading..." ] Error e -> [ H.text ("Error: " ++ httpErrorToString e) ] Success i -> [ introspectionView i ] ) ) ) Nothing -> Nothing introspectionView : Introspection -> Html Msg introspectionView i = let allTypes = List.filter (\t -> not (List.member t.name <| List.filterMap identity [ i.mutationType, i.queryType ] ) ) i.types in H.div [] (List.map typeView allTypes) typeView : QLType -> Html Msg typeView qt = H.div [] [ H.text (qt.name ++ ": " ++ qt.kind) ] httpErrorToString : Http.Error -> String httpErrorToString e = 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 "queryType" (J.field "name" J.string))) (J.maybe (J.field "mutationType" (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 = decodeArgType decodeEnumValue = 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 = 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