Created
October 15, 2017 06:10
-
-
Save ugexe/b38416163be154f0bb727cbb654c0010 to your computer and use it in GitHub Desktop.
Perl 6 module metadata microservice that uses installed modules as the data source to both RESTful-ish and GraphQL APIs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env perl6 | |
use v6; | |
use Cro::HTTP::Server; | |
use Cro::HTTP::Router; | |
use Cro::HTTP::BodyParser; | |
use GraphQL; | |
require GraphQL::GraphiQL; | |
# THE DATABASE/MODEL READS/INDEXES DIRECTLY FROM THE MODULES INSTALLED ON YOUR SYSTEM | |
## GraphQL API | |
# | |
# View name/auth/version of best-match GraphQL distribution, and it's dependencies' name/version (as if it were resolved, so usually latest version) | |
# $ curl -H "Content-Type: application/json" -X POST -d '{ "query" : "{ resolve(name: GraphQL) { name auth version dependencies { name version } } }" }' http://localhost:3000/graphql | |
# | |
# View name/auth/version of all distributions using a name starting with 'Graph', each resolved dependencies' name/version, and ordered from highest version to lowest | |
# $ curl -H "Content-Type: application/json" -X POST -d '{ "query" : "{ candidates(name: Graph) { name auth version dependencies { name version } } }" }' http://localhost:3000/graphql | |
# | |
# View all distributions name and version | |
# $ curl -H "Content-Type: application/json" -X POST -d '{ "query" : "{ installed { name version } } }" }' http://localhost:3000/graphql | |
# | |
# Demonstrate recursive relationship | |
# $ curl -H "Content-Type: application/json" -X POST -d '{ "query" : "{ resolve(name: GraphQL) { name dependencies { name dependencies { name dependencies { name } } } } }" }' http://localhost:3000/graphql | |
## RESTful-ish API | |
# | |
# View all available distributions | |
# $ curl http://localhost:3000/installed | |
# | |
# View the best match from various sources for a given distribution query (name must be exact match to query name) | |
# $ curl -H "Content-Type: application/json" -X POST -d '{ "name" : "Zef" }' http://localhost:3000/resolve | |
# | |
# View all matching distributions from various sources (name only needs start with query name) | |
# See: https://github.com/rakudo/rakudo/pull/1125 for CUR interface proposal | |
# $ curl -H "Content-Type: application/json" -X POST -d '{ "name" : "Zef" }' http://localhost:3000/candidates | |
# $ curl -H "Content-Type: application/json" -X POST -d '{ "name" : "Z" }' http://localhost:3000/candidates | |
# $ curl -H "Content-Type: application/json" -X POST -d '{ "name" : "Z", "ver" : "0.1.0+" }' http://localhost:3000/candidates | |
# Utils coming in the next major version of zef (where this code comes from) will replace these | |
# with `use Zef::Utils::Distribution` (name subject to change). | |
package Zef::Utils { | |
our &match-metas = -> CompUnit::DependencySpecification $cu-spec, %meta { | |
# Emulates the 'recommendation manager' used by rakudo/CUR. | |
my $version-matcher = ($cu-spec.version-matcher ~~ Bool) | |
?? $cu-spec.version-matcher | |
!! Version.new($cu-spec.version-matcher); | |
my $api-matcher = ($cu-spec.api-matcher ~~ Bool) | |
?? $cu-spec.api-matcher | |
!! Version.new($cu-spec.api-matcher); | |
my @names = grep { .defined }, | |
( | |
%meta<name>, | |
%meta<provides>.keys.Slip, | |
%meta<provides>.values.map({ Str ?? $_ !! Hash ?? $_.keys[0] !! Pair ?? $_.key !! Nil }).Slip, | |
%meta<files>.hash.keys.Slip | |
); | |
so (first { .starts-with($cu-spec.short-name) }, @names)\ | |
and (%meta<auth> // '') ~~ $cu-spec.auth-matcher\ | |
and Version.new(%meta<ver> // 0) ~~ $version-matcher\ | |
and Version.new(%meta<api> // 0) ~~ $api-matcher\ | |
} | |
our &sort-metas = { | |
$^a.list\ | |
==> sort { Version.new($^b<api> // 0) <=> Version.new($^a<api> // 0) }\ | |
==> sort { Version.new($^b<ver> // 0) <=> Version.new($^a<ver> // 0) } | |
} | |
our &installed = -> :$curs = $*REPO.repo-chain { | |
$curs.list\ | |
==> grep { $_ ~~ CompUnit::Repository::Installable }\ | |
==> map { $_ => $_.installed.grep(*.defined).map(*.meta.hash) }\ | |
==> hash; | |
} | |
our &resolve = -> %query-spec [:$name, :$auth, :$api, :ver(:$version)], :$curs = $*REPO.repo-chain { | |
my $cur-spec = CompUnit::DependencySpecification.new( | |
short-name => $name, | |
auth-matcher => $auth // True, | |
api-matcher => $api // True, | |
version-matcher => $version // True, | |
); | |
$curs.list\ | |
==> grep { $_ ~~ CompUnit::Repository::Installable }\ | |
==> map { .name => .resolve($cur-spec).map(*.distribution.meta.hash).cache }\ | |
==> grep { .value.values.grep(*.so) }\ | |
==> hash; | |
} | |
our &candidates = -> %query-spec [:$name, :$auth, :$api, :ver(:$version)], :$curs = $*REPO.repo-chain { | |
# See: rakudo PR https://github.com/rakudo/rakudo/pull/1125 | |
# which makes puts this block's functionality into core | |
my $cu-spec = CompUnit::DependencySpecification.new( | |
short-name => $name, | |
auth-matcher => $auth // True, | |
api-matcher => $api // True, | |
version-matcher => $version // True, | |
); | |
installed(:$curs).kv\ | |
==> map { $^a => ($^b ==> grep { match-metas($cu-spec, .hash) } ==> sort-metas) }\ | |
==> hash; | |
} | |
} | |
sub MAIN(:$host = 'localhost', :$port = 3000) { | |
my $zef-meta-service = Cro::HTTP::Server.new( | |
host => $host, | |
port => $port, | |
body-parsers => [Cro::HTTP::BodyParser::JSON], | |
application => route { | |
# Non-graphql endpoints usually return a hash where each key is a 'source' and the value is a list of hash. | |
# GraphQL endpoints usually return a list of the unique values from the source hash noted above. | |
# TODO: user submission via CUR interface | |
# get -> 'install' { | |
# # code to add the distribution to database/source | |
# } | |
# Get a hash where the keys are sources and values are distributions found in that source. | |
get -> 'installed' { | |
my %repo-dists = Zef::Utils::installed().hash; | |
content 'application/json', %repo-dists; | |
} | |
# Get a hash where the keys are sources and values are distributions found in that source. | |
# The distributions will contain the single best matching candidates, e.g. what `use Foo` decides to choose from. | |
post -> 'resolve' { | |
request-body -> %query-spec ( | |
:$name!, | |
:$auth, | |
:$api, | |
:ver(:$version), | |
) { | |
my %repo-dists = Zef::Utils::resolve(%query-spec).hash; | |
content 'application/json', %repo-dists; | |
} | |
} | |
# Get a hash where the keys are sources and values are distributions found in that source. | |
# The distributions will contain all matching candidates. | |
post -> 'candidates' { | |
request-body -> %query-spec ( | |
:$name!, | |
:$auth, | |
:$api, | |
:ver(:$version), | |
) { | |
my %repo-dists = Zef::Utils::candidates(%query-spec).hash; | |
content 'application/json', %repo-dists; | |
} | |
} | |
# Unlike the previous endpoints the GraphQL endpoints do not | |
# return List %{Str} and instead return just a list. | |
get -> 'graphql' { | |
content 'text/html', $GraphQL::GraphiQL::GraphiQL; | |
} | |
post -> 'graphql' { | |
request-body -> %request ( | |
:$query!, | |
:$operationName, | |
:%variables, | |
) { | |
# Also see: https://github.com/CurtTilmes/perl6-modules-graphql | |
my class Query { ... } | |
#| Distribution meta data | |
class Distribution { | |
has Str $.name is required; | |
has %!meta; | |
submethod BUILD(:$!name, :%!meta) { } | |
method new(%meta [:$name, *%rest]) { self.bless(:$name, :%meta) } | |
method auth(--> Str) { ~(%!meta<auth> // '') } | |
method api(--> Str) { ~(%!meta<api> // 0) } | |
method version(--> Str) { ~(%!meta<version> // %!meta<ver> // 0) } | |
method dependencies(--> Array[Distribution]) { | |
(%!meta<depends>.Slip, %!meta<build-depends>.Slip, %!meta<test-depends>.Slip).list\ | |
==> grep { .defined }\ | |
==> unique\ | |
==> map { Query.resolve(name => $_) }\ | |
==> my Distribution @result; | |
return @result; | |
} | |
} | |
class Query { | |
my &id = { "{.<name>}:auth<{.<auth> // ''}:api<{.<api> // 0}>:ver<{.<ver version>.first(*.so) // 0}>" } | |
method installed( | |
Int :$start = 0, | |
Int :$count = 100, | |
--> Array[Distribution]) { | |
Zef::Utils::installed().values\ | |
==> map { .values.Slip }\ | |
==> grep { .defined }\ | |
==> Zef::Utils::sort-metas\ | |
==> unique(:as(&id))\ | |
==> grep { $start <= $++ <= $count }\ | |
==> map { Distribution.new($_.hash) }\ | |
==> my Distribution @result; | |
return @result; | |
} | |
method candidates( | |
Str :$name!, | |
Int :$start = 0, | |
Int :$count = 100, | |
*%_, | |
--> Array[Distribution]) { | |
Zef::Utils::candidates({ :$name, |%_ }).values\ | |
==> map { .values.Slip }\ | |
==> grep { .defined }\ | |
==> Zef::Utils::sort-metas\ | |
==> unique(:as(&id))\ | |
==> grep { $start <= $++ <= $count }\ | |
==> map { Distribution.new($_.hash) }\ | |
==> my Distribution @result; | |
return @result; | |
} | |
method resolve( | |
Str :$name!, | |
*%_, | |
--> Distribution) { | |
self.candidates(:$name, |%_, :start(0), :count(1)).head | |
} | |
} | |
state $schema = GraphQL::Schema.new(Distribution, Query); | |
my $dists = $schema.execute($query); | |
content 'application/json', $dists.to-json; | |
} | |
} | |
}, | |
) andthen *.start; | |
react { | |
whenever signal(SIGINT) { | |
$zef-meta-service.stop; | |
exit; | |
} | |
} | |
} | |
BEGIN die "Run `zef install Cro --/test` first\n" if (try require ::("Cro::HTTP::Server")) ~~ Nil; | |
BEGIN die "Run `zef install GraphQL --/test` first\n" if (try require ::("GraphQL")) ~~ Nil; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment