knuckleswtf / scribe

Generate API documentation for humans from your Laravel codebase.✍
https://scribe.knuckles.wtf/laravel/
MIT License
1.59k stars 280 forks source link

using ResponseFromApiResource for response fields #738

Closed mojtabaahn closed 9 months ago

mojtabaahn commented 9 months ago

Scribe version

4.24.0

Your question

I've got an endpoint that returns two resources:

public function login(LoginRequest $request): array
{
    $request->validate();
    return [
        'token' => new AccessTokenResource($request->user()->createToken()),
        'user'  => new  UserResource($request->user()),
    ];
}

normally when we're dealing with a single resource in response, we use ResponseFromApiResource, but in such situation I don't find any documentation about it, is this even supported? something like:

#[ResponseFromApiResource(AccessTokenResource::class, PersonalAccessToken::class, key='token')]
#[ResponseFromApiResource(UserResource::class, User::class, key='user')]

or even:

#[Response(content: [
    'token' => new ResponseFromApiResource(AccessTokenResource::class, PersonalAccessToken::class),
    'user'   => new ResponseFromApiResource(UserResource::class, User::class)
], status: 200)]

Docs

shalvah commented 9 months ago

Yes, neither of these is supported.

I thought about this for a few minutes, and I don't see an elegant way for Scribe to support this at this point. Some of my concerns:

But this is suboptimal. I think this is a good candidate for a custom attribute and/or strategy. (And if you figure out an elegant way, maybe we could accept it back into Scribe later.)

You could create your own attribute that works as you desire. Assuming the second approach:

#[ResponseFromComposableApiResources([
    'token' => [AccessTokenResource::class, PersonalAccessToken::class],
    'user'   => [UserResource::class]
], status: 200)]

Not certain of the implementation, but it would look something like:

#[Attribute(Attribute::IS_REPEATABLE | ...)]
class ResponseFromComposableApiResources extends Response
{
    public function __construct(
        public array $fields,
        public int $status = 200,
        public ?string $description = '',
    )
    {
    }

    public function toArray()
    {
        return  [
            "status" => $this->status,
            "content" => $this->makeContent(),
            "description" => $this->description,
        ];
    }

    protected function makeContent()
    {
        $response = collect($this->fields)->map(function ($field) {
            $attribute = new ResponseFromApiResource (...$field);
            return (new UseResponseAttributes)->getApiResourceResponse($attribute)['content'];
        });
}

It may involve some hacking, since the right internal APIs may not be exposed, but it's not too complicated, and if you encounter a problem with the internal APIs, you can ask so I can refactor them or expose better APIs. But this should be doable. Let me know how it goes.

shalvah commented 9 months ago

The first option is also doable, but that would need a custom strategy, since Scribe's default UseResponseAttributes strategy tries to extract one response from one attribute. In that case, you can replace the default with your custom strategy that looks for attributes which specify a key as you described, and try to combine them into one response.