File Coverage

blib/lib/GraphQL/Plugin/Convert/MojoPubSub.pm
Criterion Covered Total %
statement 41 80 51.2
branch 0 20 0.0
condition 0 6 0.0
subroutine 12 15 80.0
pod 0 3 0.0
total 53 124 42.7


line stmt bran cond sub pod time code
1             package GraphQL::Plugin::Convert::MojoPubSub;
2 1     1   128711 use strict;
  1         12  
  1         32  
3 1     1   6 use warnings;
  1         5  
  1         26  
4 1     1   1493 use GraphQL::Schema;
  1         2360028  
  1         70  
5 1     1   14 use GraphQL::Debug qw(_debug);
  1         2  
  1         56  
6 1     1   8 use DateTime;
  1         2  
  1         28  
7 1     1   6 use GraphQL::Type::Scalar qw($Boolean $String);
  1         2  
  1         136  
8 1     1   9 use GraphQL::Type::Object;
  1         14  
  1         42  
9 1     1   1418 use GraphQL::Type::InputObject;
  1         96342  
  1         48  
10 1     1   1133 use GraphQL::AsyncIterator;
  1         146365  
  1         68  
11              
12             our $VERSION = "0.01";
13 1     1   13 use constant DEBUG => $ENV{GRAPHQL_DEBUG};
  1         3  
  1         66  
14 1     1   7 use constant FIREHOSE => '_firehose';
  1         3  
  1         1036  
15              
16             my ($DateTime) = grep $_->name eq 'DateTime', GraphQL::Plugin::Type->registered;
17              
18             sub field_resolver {
19 0     0 0 0 my ($root_value, $args, $context, $info) = @_;
20 0         0 my $field_name = $info->{field_name};
21 0         0 my $parent_type = $info->{parent_type}->to_string;
22             my $property = ref($root_value) eq 'HASH'
23 0 0       0 ? $root_value->{$field_name}
24             : $root_value;
25 0         0 DEBUG and _debug('MojoPubSub.resolver', $field_name, $parent_type, $args, $property, ref($root_value) eq 'HASH' ? $root_value : ());
26 0         0 my $result = eval {
27 0 0       0 return $property->($args, $context, $info) if ref $property eq 'CODE';
28 0 0       0 return $property if ref $root_value eq 'HASH';
29 0 0 0     0 if ($parent_type eq 'Query' and $field_name eq 'status') {
30             # semi-fake "status" because can't have empty query
31 0         0 return 1;
32             }
33 0 0 0     0 die "Unknown field '$field_name'\n"
34             unless $parent_type eq 'Mutation' and $field_name eq 'publish';
35 0         0 my $now = DateTime->now;
36 0 0       0 my @input = @{ $args->{input} || [] };
  0         0  
37 0         0 DEBUG and _debug('MojoPubSub.resolver(input)', @input);
38 0         0 for my $msg (@input) {
39             # regrettably blocking, until both have a notify_p
40 0         0 $msg = { dateTime => $now, %$msg };
41 0         0 $root_value->pubsub->json($_)->notify($_, $msg) for $msg->{channel}, FIREHOSE;
42             }
43 0         0 $now;
44             };
45 0 0       0 die $@ if $@;
46 0         0 $result;
47             }
48              
49             sub subscribe_resolver {
50 0     0 0 0 my ($root_value, $args, $context, $info) = @_;
51 0 0       0 my @channels = @{ $args->{channels} || [] };
  0         0  
52 0 0       0 @channels = (FIREHOSE) if !@channels;
53 0         0 my $ai = GraphQL::AsyncIterator->new(promise_code => $info->{promise_code});
54 0         0 my $field_name = $info->{field_name};
55 0         0 DEBUG and _debug('MojoPubSub.s_r', $args, \@channels);
56 0         0 my $cb;
57             my @subscriptions;
58             $cb = sub {
59 0     0   0 my ($pubsub, $msg) = @_;
60 0         0 DEBUG and _debug('MojoPubSub.cb', $msg, \@channels);
61 0         0 eval { $ai->publish({ $field_name => $msg }) };
  0         0  
62 0         0 DEBUG and _debug('MojoPubSub.cb2', $@);
63 0 0       0 return if !$@;
64 0         0 $root_value->pubsub->unlisten(@$_) for @subscriptions;
65 0         0 };
66 0         0 @subscriptions = map [ $_, $root_value->pubsub->listen($_ => $cb) ], @channels;
67 0         0 $ai;
68             }
69              
70             sub to_graphql {
71 1     1 0 535 my ($class, $fieldspec, $pubsub) = @_;
72 1         20 $fieldspec = { map +($_ => { type => $fieldspec->{$_} }), keys %$fieldspec };
73 1         25 my $input_fields = {
74             channel => { type => $String->non_null },
75             %$fieldspec,
76             };
77 1         15 DEBUG and _debug('MojoPubSub.input', $input_fields);
78 1         23 my $output_fields = {
79             channel => { type => $String->non_null },
80             dateTime => { type => $DateTime->non_null },
81             %$fieldspec,
82             };
83 1         341 DEBUG and _debug('MojoPubSub.output', $output_fields);
84 1         21 my $schema = GraphQL::Schema->new(
85             query => GraphQL::Type::Object->new(
86             name => 'Query',
87             fields => { status => { type => $Boolean->non_null } },
88             ),
89             mutation => GraphQL::Type::Object->new(
90             name => 'Mutation',
91             fields => { publish => {
92             type => $DateTime->non_null,
93             args => { input => { type => GraphQL::Type::InputObject->new(
94             name => 'MessageInput',
95             fields => $input_fields,
96             )->non_null->list->non_null } },
97             } },
98             ),
99             subscription => GraphQL::Type::Object->new(
100             name => 'Subscription',
101             fields => { subscribe => {
102             type => GraphQL::Type::Object->new(
103             name => 'Message',
104             fields => $output_fields,
105             )->non_null,
106             args => { channels => { type => $String->non_null->list } },
107             } },
108             ),
109             );
110             +{
111 1         16975 schema => $schema,
112             root_value => $pubsub,
113             resolver => \&field_resolver,
114             subscribe_resolver => \&subscribe_resolver,
115             };
116             }
117              
118             =encoding utf-8
119              
120             =head1 NAME
121              
122             GraphQL::Plugin::Convert::MojoPubSub - convert a Mojo PubSub server to GraphQL schema
123              
124             =begin markdown
125              
126             # PROJECT STATUS
127              
128             | OS | Build status |
129             |:-------:|--------------:|
130             | Linux | [![Build Status](https://travis-ci.org/graphql-perl/GraphQL-Plugin-Convert-MojoPubSub.svg?branch=master)](https://travis-ci.org/graphql-perl/GraphQL-Plugin-Convert-MojoPubSub) |
131              
132             [![CPAN version](https://badge.fury.io/pl/GraphQL-Plugin-Convert-MojoPubSub.svg)](https://metacpan.org/pod/GraphQL::Plugin::Convert::MojoPubSub) [![Coverage Status](https://coveralls.io/repos/github/graphql-perl/GraphQL-Plugin-Convert-MojoPubSub/badge.svg?branch=master)](https://coveralls.io/github/graphql-perl/GraphQL-Plugin-Convert-MojoPubSub?branch=master)
133              
134             =end markdown
135              
136             =head1 SYNOPSIS
137              
138             use GraphQL::Plugin::Convert::MojoPubSub;
139             use GraphQL::Type::Scalar qw($String);
140             my $pg = Mojo::Pg->new('postgresql://postgres@/test');
141             my $converted = GraphQL::Plugin::Convert::MojoPubSub->to_graphql(
142             {
143             username => $String->non_null,
144             message => $String->non_null,
145             },
146             $pg->pubsub,
147             );
148             print $converted->{schema}->to_doc;
149              
150             =head1 DESCRIPTION
151              
152             This module implements the L<GraphQL::Plugin::Convert> API to convert
153             a Mojo pub-sub server (currently either L<Mojo::Pg::PubSub> or
154             L<Mojo::Redis::PubSub>) to L<GraphQL::Schema> with publish/subscribe
155             functionality.
156              
157             =head1 ARGUMENTS
158              
159             To the C<to_graphql> method:
160              
161             =over
162              
163             =item *
164              
165             a hash-ref of field-names to L<GraphQL::Type> objects. These must be
166             both input and output types, so only scalars or enums. This allows you
167             to pass in programmatically-created scalars or enums.
168              
169             This will be used to construct the C<fields> arguments for the
170             L<GraphQL::Type::InputObject> and L<GraphQL::Type::Object> which are
171             the input and output of the mutation and subscription respectively.
172              
173             =item *
174              
175             an object compatible with L<Mojo::Redis>, with a C<pubsub> attribute.
176              
177             =back
178              
179             Note the output type will have a C<dateTime> field added to it with type
180             non-null C<DateTime>. Both input and output types will have a non-null
181             C<channel> C<String> added.
182              
183             E.g. for this input (implementing a trivial chat system):
184              
185             {
186             username => $String->non_null,
187             message => $String->non_null,
188             }
189              
190             The schema will look like:
191              
192             scalar DateTime
193              
194             input MessageInput {
195             channel: String!
196             username: String!
197             message: String!
198             }
199              
200             type Message {
201             channel: String!
202             username: String!
203             message: String!
204             dateTime: DateTime!
205             }
206              
207             type Query {
208             status: Boolean!
209             }
210              
211             type Mutation {
212             publish(input: [MessageInput!]!): DateTime!
213             }
214              
215             type Subscription {
216             subscribe(channels: [String!]): Message!
217             }
218              
219             The C<subscribe> field takes a list of channels to subscribe to. If the
220             list is null or empty, all channels will be subscribed to - a "firehose",
221             implemented as an actual channel named C<_firehose>.
222              
223             =head1 DEBUGGING
224              
225             To debug, set environment variable C<GRAPHQL_DEBUG> to a true value.
226              
227             =head1 AUTHOR
228              
229             Ed J, C<< <etj at cpan.org> >>
230              
231             =head1 LICENSE
232              
233             Copyright (C) Ed J
234              
235             This library is free software; you can redistribute it and/or modify
236             it under the same terms as Perl itself.
237              
238             =cut
239              
240             1;