Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"keywords": ["module", "xp"],
"require" : {
"xp-framework/core": "^11.0 | ^10.0 | ^9.0 | ^8.0 | ^7.0",
"xp-framework/reflection": "^2.0",
"xp-forge/web": "^3.0 | ^2.0 | ^1.0",
"xp-forge/marshalling": "^1.0 | ^0.3 | ^0.2",
"xp-forge/json": "^5.0 | ^4.0 | ^3.1",
Expand Down
55 changes: 27 additions & 28 deletions src/main/php/web/rest/Delegate.class.php
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<?php namespace web\rest;

use io\streams\{InputStream, Streams};
use lang\IllegalArgumentException;
use lang\reflect\TargetInvocationException;
use lang\reflection\{Method, TargetException};
use lang\{IllegalArgumentException, Reflection, Type};
use web\Request;

class Delegate {
Expand Down Expand Up @@ -31,67 +31,66 @@ static function __static() {
* Creates a new delegate
*
* @param object $instance
* @param lang.reflect.Method $method
* @param string|lang.reflection.Method $method
* @param string $source Default source
*/
public function __construct($instance, $method, $source) {
$this->instance= $instance;
$this->method= $method;
foreach ($method->getParameters() as $param) {
$this->method= $method instanceof Method ? $method : Reflection::type($instance)->method($method);
foreach ($this->method->parameters() as $param) {

// Source explicitely set by annotation
foreach ($param->getAnnotations() as $source => $name) {
if (isset(self::$SOURCES[$source])) {
$this->param($param, $name ?? $param->getName(), $source);
foreach ($param->annotations() as $annotation) {
if ($accessor= self::$SOURCES[$annotation->name()] ?? null) {
$this->param($param, $name ?? $param->name(), $accessor);
continue 2;
}
}

// Source derived from parameter type
$type= $param->getType();
if ('var' === $type->getName()) {
$type= $param->constraint()->type();
if (Type::$VAR === $type) {
// NOOP
} else if ($type->isAssignableFrom(InputStream::class)) {
$source= 'stream';
} else if ($type->isAssignableFrom(Request::class)) {
$source= 'request';
}
$this->param($param, $param->getName(), $source);
$this->param($param, $param->name(), self::$SOURCES[$source]);
}
}

/**
* Adds parameter request reader for a given parameter
* Adds parameter request accessor for a given parameter
*
* @param lang.reflect.Parameter $param
* @param lang.reflection.Parameter $param
* @param string $name
* @param function(web.Request, web.rest.format.EntityFormat, string): var $source
* @param function(web.Request, web.rest.format.EntityFormat, string): var $accessor
* @return void
* @throws lang.IllegalArgumentException
*/
private function param($param, $name, $source) {
$extract= self::$SOURCES[$source];

if ($param->isOptional()) {
$default= $param->getDefaultValue();
$read= function($req, $format) use($extract, $name, $default) {
return $extract($req, $format, $name) ?? $default;
private function param($param, $name, $accessor) {
if ($param->optional()) {
$default= $param->default();
$read= function($req, $format) use($accessor, $name, $default) {
return $accessor($req, $format, $name) ?? $default;
};
} else {
$read= function($req, $format) use($extract, $name) {
if (null === ($value= $extract($req, $format, $name))) {
$read= function($req, $format) use($accessor, $name) {
if (null === ($value= $accessor($req, $format, $name))) {
throw new IllegalArgumentException('Missing argument '.$name);
}
return $value;
};
}
$this->params[$name]= ['type' => $param->getType(), 'read' => $read];
$this->params[$name]= ['type' => $param->constraint()->type(), 'read' => $read];
}

/** @return string */
public function name() { return nameof($this->instance).'::'.$this->method->getName(); }
public function name() { return nameof($this->instance).'::'.$this->method->name(); }

/** @return [:var] */
public function annotations() { return $this->method->getAnnotations(); }
/** @return lang.reflection.Annotations */
public function annotations() { return $this->method->annotations(); }

/** @return [:var] */
public function params() { return $this->params; }
Expand All @@ -106,7 +105,7 @@ public function params() { return $this->params; }
public function invoke($args) {
try {
return $this->method->invoke($this->instance, $args);
} catch (TargetInvocationException $e) {
} catch (TargetException $e) {
throw $e->getCause();
}
}
Expand Down
13 changes: 9 additions & 4 deletions src/main/php/web/rest/Delegates.class.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?php namespace web\rest;

use lang\IllegalArgumentException;
use lang\{IllegalArgumentException, Reflection};

/**
* Matches request and routes to correct delegate
Expand Down Expand Up @@ -31,16 +31,21 @@ public function with($instance, $base= '/') {
}

$base= rtrim($base, '/');
foreach (typeof($instance)->getMethods() as $method) {
foreach (array_intersect_key($method->getAnnotations(), self::$METHODS) as $verb => $segment) {
foreach (Reflection::type($instance)->methods() as $method) {

foreach ($method->annotations() as $annotation) {
$verb= $annotation->name();
if (null === ($source= self::$METHODS[$verb] ?? null)) continue;

$segment= $annotation->argument(0);
if (null === $segment) {
$pattern= $base.'(/.+)?';
} else if ('/' === $segment || '' === $segment) {
$pattern= $base.'/?';
} else {
$pattern= $base.preg_replace(['/\{([^:}]+):([^}]+)\}/', '/\{([^}]+)\}/'], ['(?<$1>$2)', '(?<$1>[^/]+)'], $segment);
}
$this->patterns['#^'.$verb.$pattern.'$#']= new Delegate($instance, $method, self::$METHODS[$verb]);
$this->patterns['#^'.$verb.$pattern.'$#']= new Delegate($instance, $method, $source);
}
}
return $this;
Expand Down
10 changes: 8 additions & 2 deletions src/main/php/web/rest/MethodsIn.class.php
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
<?php namespace web\rest;

use lang\Reflection;

/**
* Creates routing based on a given instance
*/
class MethodsIn extends Delegates {

/** @param object $instance */
public function __construct($instance) {
$class= typeof($instance);
$this->with($instance, $class->hasAnnotation('resource') ? $class->getAnnotation('resource') ?? '' : '/');
$class= Reflection::type($instance);
if ($annotation= $class->annotation(Resource::class)) {
$this->with($instance, (string)$annotation->argument(0));
} else {
$this->with($instance, '/');
}
uksort($this->patterns, function($a, $b) { return strlen($b) - strlen($a); });
}
}
14 changes: 7 additions & 7 deletions src/main/php/web/rest/ResourcesIn.class.php
Original file line number Diff line number Diff line change
@@ -1,25 +1,25 @@
<?php namespace web\rest;

use lang\reflect\Package;
use lang\reflection\Package;

/**
* Creates routing based on resource classes in a given package
*
* @test xp://web.rest.unittest.ResourcesInTest
* @test web.rest.unittest.ResourcesInTest
*/
class ResourcesIn extends Delegates {

/**
* Creates this delegates instance
*
* @param lang.reflect.Package|string $package
* @param lang.reflection.Package|string $package
* @param function(lang.XPClass): object $new Optional function to create instances
*/
public function __construct($package, $new= null) {
$p= $package instanceof Package ? $package : Package::forName($package);
foreach ($p->getClasses() as $class) {
if ($class->hasAnnotation('resource')) {
$this->with($new ? $new($class) : $class->newInstance(), $class->getAnnotation('resource') ?? '');
$p= $package instanceof Package ? $package : new Package($package);
foreach ($p->types() as $type) {
if ($resource= $type->annotation(Resource::class)) {
$this->with($new ? $new($type->class()) : $type->newInstance(), (string)$resource->argument(0));
}
}
uksort($this->patterns, function($a, $b) { return strlen($b) - strlen($a); });
Expand Down
6 changes: 3 additions & 3 deletions src/test/php/web/rest/unittest/InvocationsTest.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

use lang\{ElementNotFoundException, IllegalStateException};
use test\{Assert, Test};
use web\rest\unittest\api\Users;
use web\rest\unittest\api\{Users, Cached};
use web\rest\{Interceptor, Response, RestApi};

class InvocationsTest extends RunTest {
Expand Down Expand Up @@ -49,12 +49,12 @@ public function intercepting_catching_exceptions() {
#[Test]
public function intercepting_can_access_annotations() {
$invocations= function($invocation, $args) use(&$cached) {
$cached= $invocation->target()->annotations()['cached'];
$cached= $invocation->target()->annotations()->type(Cached::class);
return $invocation->proceed($args);
};

$this->run((new RestApi(new Users()))->intercepting($invocations), 'GET', '/users/1549/avatar');
Assert::equals(['ttl' => 3600], $cached);
Assert::equals(['ttl' => 3600], $cached->arguments());
}

#[Test]
Expand Down
4 changes: 2 additions & 2 deletions src/test/php/web/rest/unittest/ResourcesInTest.class.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?php namespace web\rest\unittest;

use lang\reflect\Package;
use lang\reflection\Package;
use test\{Assert, Test};
use web\rest\ResourcesIn;

Expand All @@ -14,7 +14,7 @@ public function using_package_name() {

#[Test]
public function using_package_instance() {
$r= new ResourcesIn(Package::forName('web.rest.unittest.api'));
$r= new ResourcesIn(new Package('web.rest.unittest.api'));
Assert::notEquals(null, $r->target('get', '/monitoring/status'));
}

Expand Down