Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
93.55% covered (success)
93.55%
29 / 31
71.43% covered (warning)
71.43%
5 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
ApiClient
93.55% covered (success)
93.55%
29 / 31
71.43% covered (warning)
71.43%
5 / 7
8.02
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 setLogger
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 makeQueryString
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
 makeApiCall
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 getToolByName
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 getListById
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 findTools
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2declare( strict_types = 1 );
3/**
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation; either version 2 of the License, or
7 * (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License along
15 * with this program; if not, write to the Free Software Foundation, Inc.,
16 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17 *
18 * @file
19 */
20
21namespace MediaWiki\Extension\Toolhub;
22
23use MediaWiki\Http\HttpRequestFactory;
24use Psr\Log\LoggerAwareInterface;
25use Psr\Log\LoggerInterface;
26use Psr\Log\NullLogger;
27
28/**
29 * Toolhub API client.
30 *
31 * @copyright © 2022 Wikimedia Foundation and contributors
32 */
33class ApiClient implements LoggerAwareInterface {
34
35    /** @var HttpRequestFactory */
36    private $requestFactory;
37    /** @var string */
38    private $baseUrl;
39    /** @var LoggerInterface */
40    private $logger;
41
42    /**
43     * Constructor.
44     *
45     * @param HttpRequestFactory $requestFactory
46     * @param string $baseUrl
47     */
48    public function __construct(
49        HttpRequestFactory $requestFactory,
50        string $baseUrl
51    ) {
52        $this->requestFactory = $requestFactory;
53        $this->baseUrl = $baseUrl;
54        $this->logger = new NullLogger;
55    }
56
57    public function setLogger( LoggerInterface $logger ): void {
58        $this->logger = $logger;
59    }
60
61    /**
62     * Build a query string from an array of key => value pairs.
63     *
64     * @param array $params
65     * @return string URL encoded query string
66     */
67    public static function makeQueryString( array $params ): string {
68        $params = array_filter(
69            $params,
70            static function ( $v ) {
71                return $v !== null;
72            }
73        );
74        ksort( $params );
75        return http_build_query( $params );
76    }
77
78    /**
79     * Perform an HTTP request.
80     *
81     * @param string $verb HTTP verb
82     * @param string $url Full URL including query string if needed
83     * @param array $opts See HttpRequestFactory::create
84     * @param string $caller The method making this request, for profiling
85     * @return array Response data
86     */
87    public function makeApiCall(
88        string $verb,
89        string $url,
90        array $opts,
91        string $caller
92    ): array {
93        // FIXME: add defaults to $opts:
94        // Accept header
95        // Content-Type header
96        // Accept-Language header?
97        // User-Agent header
98        $resp = $this->requestFactory->request( $verb, $url, $opts, $caller );
99        if ( $resp === null ) {
100            // FIXME: what should really happen here?
101            return [ 'error' => 'Got a null response so BOOM!' ];
102        }
103        return json_decode( $resp, true );
104    }
105
106    /**
107     * Get info for a specific tool.
108     *
109     * @param string $name Name of the tool
110     * @return array
111     */
112    public function getToolByName( string $name ): array {
113        $escName = urlencode( $name );
114        $req = "{$this->baseUrl}/api/tools/{$escName}/";
115        return $this->makeApiCall( 'GET', $req, [], __METHOD__ );
116    }
117
118    /**
119     * Get info for a specific list.
120     *
121     * @param int $id List id
122     * @return array
123     */
124    public function getListById( int $id ): array {
125        $req = "{$this->baseUrl}/api/lists/{$id}/";
126        return $this->makeApiCall( 'GET', $req, [], __METHOD__ );
127    }
128
129    /**
130     * Search for tools.
131     *
132     * @param ?string $query User provided query
133     * @param int $page Result page
134     * @param int $pageSize Number of tools per page
135     * @return array
136     */
137    public function findTools(
138        ?string $query = null,
139        int $page = 1,
140        int $pageSize = 25
141    ): array {
142        $qs = self::makeQueryString(
143            [
144                'q' => $query,
145                'ordering' => '-score',
146                'page' => $page,
147                'page_size' => $pageSize,
148            ]
149        );
150        $req = "{$this->baseUrl}/api/search/tools/?{$qs}";
151        return $this->makeApiCall( 'GET', $req, [], __METHOD__ );
152    }
153}