File Coverage

blib/lib/GraphQL/Plugin/Convert/MojoPubSub.pm
Criterion Covered Total %
statement 44 83 53.0
branch 0 20 0.0
condition 0 6 0.0
subroutine 13 16 81.2
pod 0 3 0.0
total 57 128 44.5


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