bliako has asked for the wisdom of the Perl Monks concerning the following question:

Esteemed Monks,

I am trying to create a Perl client for OpenTripPlanner (OTP). OTP can plan a public-transport trip (buses, trains, bikes, etc.). It is a real gem in the FOSS family. (For those interested this tutorial will setup a local server in 10 minutes: https://docs.opentripplanner.org/en/latest/Basic-Tutorial/. Including data and an underlying (OpenStreet)map layer for visualising the trips. Btw the data that feeds the beast - schedules etc. - is mostly public domain, in GTFS format, offered freely by transport agencies around the world and so is the optional underlying map (Open Street Map, another gem in FOSS).

An OTP server accepts queries in GraphQL and returns results back in JSON (I am very new to this GraphQL, so take these introductory with a pinch of salt). To get me started I have handcrafted some Perl classes corresponding to returned GraphQL objects (which in the returned JSON are hashtables).

After the investigation period, I am now retrieving the whole OTP GraphQL schema (see this SO answer for how but mostly this: https://medium.com/@mrthankyou/how-to-get-a-graphql-schema-28915025de0e) as JSON and I need to convert this schema to Perl Classes which each can take a JSON response and populate their corresponding object with that.

I have discovered a Perl module, GraphQL by ED J/etj++, which should be able to do what I need but it is unclear to me as to the how. Ideally I would like to feed it with the OTP GraphQL schema and let it create and write all the Perl Classes. Is this possible and how?

My OTP GraphQL schema is huge so I am not posting it here, but here is a CLI one-liner to retrieve one from local OTP server, if you have setup one -- or I can post my schema (350Kb) to my scratchpad and erase it in due time, let me know:"

curl \ --request POST --url 'http://localhost:8080/otp/gtfs/v1' \ --header 'Content-Type: application/json' \ --header 'OTPTimeout: 180000' \ --data '{"query":"query IntrospectionQuery { __schema { queryType +{ name } mutationType { name } subscriptionType { name } types { ...F +ullType } directives { name description locations args { ...InputValu +e } } } } fragment FullType on __Type { kind name description fields( +includeDeprecated: true) { name description args { ...InputValue } ty +pe { ...TypeRef } isDeprecated deprecationReason } inputFields { ...I +nputValue } interfaces { ...TypeRef } enumValues(includeDeprecated: t +rue) { name description isDeprecated deprecationReason } possibleType +s { ...TypeRef } } fragment InputValue on __InputValue { name descrip +tion type { ...TypeRef } defaultValue } fragment TypeRef on __Type { +kind name ofType { kind name ofType { kind name ofType { kind name of +Type { kind name ofType { kind name ofType { kind name ofType { kind +name } } } } } } } } "}'

Update:

I have managed to convert the JSON schema into SDL (using graphql-introspection-json-to-sdl schema.json > schema.sdl which is a JS cli script installed with npm). Then I tried:

use GraphQL::Schema; my $FH; open($FH, '<:utf8', 'schema.sdl') or die "failed to open SDL file, $!" +; my $SDLcontent; { local $/ = undef; $SDLcontent = <$FH>; } close $FH; my $schema = GraphQL::Schema->from_doc($SDLcontent); if( ! defined $schema ){ die "failed to parse SDL schema." }

alas no joy because thunking failed:

Reference {"groupSimilarityKeepOne" => {"default_value" => "0.85","des +...} did not pass type constraint "CodeLike|FieldMapInput" (in $self- +>{"fields"}) at MooX/ Thunking.pm line 43 "CodeLike|FieldMapInput" requires that the value pass "CodeLike" o +r "FieldMapInput" Reference {"groupSimilarityKeepOne" => {"default_value" => "0.85", +"des...} did not pass type constraint "CodeLike" Reference {"groupSimilarityKeepOne" => {"default_value" => "0. +85","des...} did not pass type constraint "CodeLike" "CodeLike" is defined as: do { package Type::Tiny; ref($_) eq +q[CODE] or Scalar::Util::blessed($_) && (sub { require overload; over +load::Overloaded(ref $_[0] or $_[0]) and overload::Method((ref $_[0] +or $_[0]), $_[1]) })->($_, q[&{}]) } Reference {"groupSimilarityKeepOne" => {"default_value" => "0.85", +"des...} did not pass type constraint "FieldMapInput" "FieldMapInput" is a subtype of "Map[StrNameValid,Dict[default +_value=>Optional[Any],description=>Optional[Str],directives=>Optional +[ArrayRef[HashRef]],type=>ConsumerOf["GraphQL::Role::Input"]]]&__ANON +__" "Map[StrNameValid,Dict[default_value=>Optional[Any],descriptio +n=>Optional[Str],directives=>Optional[ArrayRef[HashRef]],type=>Consum +erOf["GraphQL::Role::Input"]]]&__ANON__" requires that the value pass + "Map[StrNameValid,Dict[default_value=>Optional[Any],description=>Opt +ional[Str],directives=>Optional[ArrayRef[HashRef]],type=>ConsumerOf[" +GraphQL::Role::Input"]]]" and "__ANON__" Reference {"groupSimilarityKeepOne" => {"default_value" => "0. +85","des...} did not pass type constraint "__ANON__" is defined as: ((do { package Type::Tiny; (ref($_) +eq 'HASH') and do { my $ok = 1; for my $i (values %{$_}) { ($ok = 0, +last) unless do { package Type::Tiny; (ref($i) eq 'HASH') and exists( +$i->{"type"}) and (do { use Scalar::Util (); Scalar::Util::blessed($i +->{"type"}) and do { my $method = $i->{"type"}->can('DOES')||$i->{"ty +pe"}->can('isa'); $i->{"type"}->$method(q[GraphQL::Role::Input]) } }) + } }; $ok } }) && do { package Type::Tiny; !grep { $_->{default_value} and !$_->{type}->is_valid($_->{default_v +alue}) } values %{$_} })

The offending content is:

""" Settings that control the behavior of itinerary filtering. **These are + advanced settings and should not be set by a user through user preferences.** """ input PlanItineraryFilterInput { """ Pick one itinerary from each group after putting itineraries that ar +e `85%` similar together, if the given value is `0.85`, for example. Itineraries are grouped t +ogether based on relative the distance of transit travel that is identical between the itinera +ries (access, egress and transfers are ignored). The value must be at least `0.5`. """ groupSimilarityKeepOne: Ratio = 0.85 """ Pick three itineraries from each group after putting itineraries tha +t are `68%` similar together, if the given value is `0.68`, for example. Itineraries are grouped t +ogether based on relative the distance of transit travel that is identical between the itinera +ries (access, egress and transfers are ignored). The value must be at least `0.5`. """ groupSimilarityKeepThree: Ratio = 0.68 """ Of the itineraries grouped to maximum of three itineraries, how much + worse can the non-grouped legs be compared to the lowest cost. `2.0` means that they can be do +uble the cost, and any itineraries having a higher cost will be filtered away. Use a value +lower than `1.0` to turn the grouping off. """ groupedOtherThanSameLegsMaxCostMultiplier: Float = 2 """ Itinerary filter debug profile used to control the behaviour of itin +erary filters. """ itineraryFilterDebugProfile: ItineraryFilterDebugProfile = OFF }

Thunked indeed

p.s. I put the extra-wide code into readmore tags hoping it will be more readable

Replies are listed 'Best First'.
Re: Converting a GraphQL schema into Perl classes
by etj (Priest) on Aug 30, 2024 at 15:48 UTC
    Thunking is a way of deferring evaluation, so for instance you can specify a GraphQL thing that has a field that's an array of those same things even though at that moment the "thing" isn't valid yet.
    I need to convert this schema to Perl Classes which each can take a JSON response and populate their corresponding object with that.
    I don't believe that you do need that. What would you gain by doing so? GraphQL is all about APIs that can evolve without major-versioning, which means that these classes would need to constantly change too. I made the Perl GraphQL using OO because it seemed like a good idea, and I'd do it that way again. But I believe that OO programming must earn its keep, not be the default.

    Your specific issue here may be that those "Ratio" things are GraphQL "scalar" types, and the Perl code isn't really set up to know what to do with those just with SDL information. In particular, the Perl GraphQL modules are very much for making a server-side service, and you are making client-side code. That doesn't need to know the whole schema. It only needs to know the particular query/queries to fill its own information needs. See, as linked in the "SEE ALSO" section of the GraphQL docs, http://graphql.org/graphql-js/ and the graphql-perl port at http://blogs.perl.org/users/ed_j/2017/10/graphql-perl---graphql-js-tutorial-translation-to-graphql-perl-and-mojoliciousplugingraphql.html.

      thanks for your answer. I beg to differ on the merits of OO even in a lowly client. I prefer to have a nested bunch of OO objects which know how to print themselves rather than printing a hash which can be different given different input toggles. Or they know how to return their part of a bigger query. Anyway, in prototyping I worked with the json/perlhash and know that that will not fly in the long run re:maintenance and brain rot.

      On the particular error, there is a declaration: scalar Ratio but that does not seem to be picked up. I have replaced Ratio with a Float but then other errors occured as there are quite a few typedefs like Ratio.