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    /**
58     * @param LoggerInterface $logger
59     */
60    public function setLogger( LoggerInterface $logger ) {
61        $this->logger = $logger;
62    }
63
64    /**
65     * Build a query string from an array of key => value pairs.
66     *
67     * @param array $params
68     * @return string URL encoded query string
69     */
70    public static function makeQueryString( array $params ): string {
71        $params = array_filter(
72            $params,
73            static function ( $v ) {
74                return $v !== null;
75            }
76        );
77        ksort( $params );
78        return http_build_query( $params );
79    }
80
81    /**
82     * Perform an HTTP request.
83     *
84     * @param string $verb HTTP verb
85     * @param string $url Full URL including query string if needed
86     * @param array $opts See HttpRequestFactory::create
87     * @param string $caller The method making this request, for profiling
88     * @return array Response data
89     */
90    public function makeApiCall(
91        string $verb,
92        string $url,
93        array $opts,
94        string $caller
95    ): array {
96        // FIXME: add defaults to $opts:
97        // Accept header
98        // Content-Type header
99        // Accept-Language header?
100        // User-Agent header
101        $resp = $this->requestFactory->request( $verb, $url, $opts, $caller );
102        if ( $resp === null ) {
103            // FIXME: what should really happen here?
104            return [ 'error' => 'Got a null response so BOOM!' ];
105        }
106        return json_decode( $resp, true );
107    }
108
109    /**
110     * Get info for a specific tool.
111     *
112     * @param string $name Name of the tool
113     * @return array
114     */
115    public function getToolByName( string $name ): array {
116        $escName = urlencode( $name );
117        $req = "{$this->baseUrl}/api/tools/{$escName}/";
118        return $this->makeApiCall( 'GET', $req, [], __METHOD__ );
119    }
120
121    /**
122     * Get info for a specific list.
123     *
124     * @param int $id List id
125     * @return array
126     */
127    public function getListById( int $id ): array {
128        $req = "{$this->baseUrl}/api/lists/{$id}/";
129        return $this->makeApiCall( 'GET', $req, [], __METHOD__ );
130    }
131
132    /**
133     * Search for tools.
134     *
135     * @param ?string $query User provided query
136     * @param int $page Result page
137     * @param int $pageSize Number of tools per page
138     * @return array
139     */
140    public function findTools(
141        ?string $query = null,
142        int $page = 1,
143        int $pageSize = 25
144    ): array {
145        $qs = self::makeQueryString(
146            [
147                'q' => $query,
148                'ordering' => '-score',
149                'page' => $page,
150                'page_size' => $pageSize,
151            ]
152        );
153        $req = "{$this->baseUrl}/api/search/tools/?{$qs}";
154        return $this->makeApiCall( 'GET', $req, [], __METHOD__ );
155    }
156}