Skip to content

Instantly share code, notes, and snippets.

@ugexe
Created October 15, 2017 06:10
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ugexe/b38416163be154f0bb727cbb654c0010 to your computer and use it in GitHub Desktop.
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
#!/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