diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f603f2c..2b074c3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,88 +2,170 @@ name: CI on: push: - branches: [ main, 'legacy/v2.x', 'feature/**' ] + branches: + - 'main' + - 'legacy/*' # Legacy branches: legacy/v2.x + - 'feature/*' # Feature branches: feature/new-feature + - 'hotfix/*' # Hotfix branches: hotfix/urgent-fix + - 'release/*' # Release branches: release/v3.0.0 pull_request: - branches: [ main, 'legacy/v2.x' ] - workflow_dispatch: - inputs: - php_version: - description: 'PHP version to test (optional, tests all if empty)' - required: false - default: '' - dependencies: - description: 'Dependencies type' - required: false - default: 'stable' - type: choice - options: - - stable - - lowest - - dev + branches: + - 'main' + - 'legacy/*' # PRs to legacy branches + workflow_dispatch: # Allows manual triggering via GitHub UI jobs: - tests: + test: runs-on: ubuntu-latest - + strategy: fail-fast: false matrix: - php-version: ['7.3', '7.4', '8.0', '8.1', '8.2', '8.3', '8.4'] - dependencies: ['stable'] include: - # Test with lowest dependencies - - php-version: '7.3' - dependencies: 'lowest' - - php-version: '8.4' - dependencies: 'lowest' - # Test with development dependencies - - php-version: '8.4' - dependencies: 'dev' - # Test with PHP 8.5 nightly (allow failure) - - php-version: '8.5' - dependencies: 'stable' - - continue-on-error: ${{ matrix.dependencies == 'dev' || matrix.php-version == '8.5' }} - - name: PHP ${{ matrix.php-version }} - ${{ matrix.dependencies }} + # Core PHP version testing + - php: '8.1' + allowed-to-fail: false + - php: '8.2' + allowed-to-fail: false + - php: '8.3' + allowed-to-fail: false + - php: '8.4' + allowed-to-fail: false + + # Future-ready: PHP 8.5 (alpha/dev) - when available + - php: '8.5' + stability: 'dev' + allowed-to-fail: true + + # Development stability tests + - php: '8.4' + stability: 'dev' + allowed-to-fail: true + - php: '8.5' + stability: 'dev' + allowed-to-fail: true + + name: "PHP ${{ matrix.php }}${{ matrix.stability && format(' | {0}', matrix.stability) || '' }}" + + continue-on-error: ${{ matrix.allowed-to-fail }} + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: json, mbstring, tokenizer + coverage: xdebug + tools: composer:v2 + + - name: Get composer cache directory + id: composer-cache + run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + + - name: Cache composer dependencies + uses: actions/cache@v4 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} + restore-keys: ${{ runner.os }}-composer- + + - name: Configure stability + if: matrix.stability + run: | + composer config minimum-stability ${{ matrix.stability }} + composer config prefer-stable true + + - name: Remove composer.lock + run: rm -f composer.lock + + - name: Install dependencies + run: composer update --prefer-dist --no-interaction --no-progress + + - name: Validate composer.json and composer.lock + run: composer validate --strict + + - name: Run code style check + run: composer cs + + - name: Run static analysis + run: composer analyse + + - name: Run tests + run: composer test + + code-quality: + runs-on: ubuntu-latest + name: Code Quality Checks steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup PHP - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ matrix.php-version }} - extensions: dom, curl, libxml, mbstring, zip, json - coverage: none - tools: composer:v2 - - - name: Cache Composer packages - id: composer-cache - uses: actions/cache@v4 - with: - path: ~/.composer/cache - key: ${{ runner.os }}-php-${{ matrix.php-version }}-${{ matrix.dependencies }}-${{ hashFiles('**/composer.lock') }} - restore-keys: | - ${{ runner.os }}-php-${{ matrix.php-version }}-${{ matrix.dependencies }}- - - - name: Install dependencies (lowest) - if: matrix.dependencies == 'lowest' - run: composer update --prefer-lowest --prefer-stable --prefer-dist --no-interaction - - - name: Install dependencies (stable) - if: matrix.dependencies == 'stable' - run: composer update --prefer-stable --prefer-dist --no-interaction - - - name: Install dependencies (dev) - if: matrix.dependencies == 'dev' - run: | - composer config minimum-stability dev - composer update --prefer-dist --no-interaction - - - name: Validate composer.json - run: composer validate --strict --no-check-lock - - - name: Run PHPUnit tests - run: vendor/bin/phpunit + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.4' + extensions: json, mbstring, tokenizer + coverage: none + tools: composer:v2 + + - name: Get composer cache directory + id: composer-cache + run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + + - name: Cache composer dependencies + uses: actions/cache@v4 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} + restore-keys: ${{ runner.os }}-composer- + + - name: Install dependencies + run: composer install --prefer-dist --no-interaction --no-progress + + - name: Validate composer.json and composer.lock + run: composer validate --strict + + coverage: + runs-on: ubuntu-latest + name: Code Coverage + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.4' + extensions: json, mbstring, tokenizer + coverage: xdebug + tools: composer:v2 + + - name: Get composer cache directory + id: composer-cache + run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + + - name: Cache composer dependencies + uses: actions/cache@v4 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} + restore-keys: ${{ runner.os }}-composer- + + - name: Install dependencies + run: composer install --prefer-dist --no-interaction --no-progress + + - name: Run tests with coverage + run: vendor/bin/phpunit --coverage-clover coverage.xml + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v4 + with: + file: ./coverage.xml + flags: unittests + name: codecov-umbrella + fail_ci_if_error: false diff --git a/.gitignore b/.gitignore index a151fc7..2f12f36 100644 --- a/.gitignore +++ b/.gitignore @@ -3,13 +3,14 @@ composer.lock # PHPUnit -.phpunit/ .phpunit.result.cache .phpunit.cache/ -# IDE files -.idea/ -.vscode/ +# PHPStan +.phpstan/ + +# PHP CS Fixer +.php-cs-fixer.cache # Coverage reports coverage/ diff --git a/.markdownlint.json b/.markdownlint.json index 273eaf7..38064c5 100644 --- a/.markdownlint.json +++ b/.markdownlint.json @@ -2,6 +2,5 @@ "MD024": { "siblings_only": true }, - "MD032": false, "MD013": false } diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php new file mode 100644 index 0000000..3d0ed82 --- /dev/null +++ b/.php-cs-fixer.php @@ -0,0 +1,17 @@ +in(__DIR__.'/src') + ->in(__DIR__.'/tests') + ->exclude('vendor'); + +$config = new PhpCsFixer\Config(); +$config->setFinder($finder) + ->setRules([ + '@PSR12' => true, + '@PSR12:risky' => true, + ]) + ->setRiskyAllowed(true) + ->setUnsupportedPhpVersionAllowed(true); + +return $config; diff --git a/CHANGELOG.md b/CHANGELOG.md index 84cad13..d6beb7e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,13 +5,46 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [3.0.0](https://github.com/calliostro/php-discogs-api/releases/tag/v3.0.0) – 2025-09-08 + +### Added + +- Ultra-lightweight 2-class architecture: `ClientFactory` and `DiscogsApiClient` +- Magic method API calls: `$client->artistGet(['id' => '108713'])` +- Complete API coverage: 65+ endpoints across all Discogs areas +- Multiple authentication methods: OAuth, Personal Token, or anonymous +- Modern PHP 8.1–8.5 support with strict typing +- 100% test coverage with 43 comprehensive tests +- PHPStan Level 8 static analysis +- GitHub Actions CI with multi-version testing and enhanced branch support +- Codecov integration for code coverage reporting + +### Changed + +- **BREAKING**: Namespace changed from `Discogs\*` to `Calliostro\Discogs\*` +- **BREAKING**: API surface changed from Guzzle Services to magic methods +- **BREAKING**: Minimum PHP version now 8.1+ (was 7.3) +- Simplified dependencies: removed Guzzle Services, Command, OAuth Subscriber +- Replace `squizlabs/php_codesniffer` with `friendsofphp/php-cs-fixer` for code style checking +- Update code style standard from PSR-12 via PHPCS to PSR-12 via PHP-CS-Fixer +- Add `.php-cs-fixer.php` configuration file with PSR-12 rules +- Update composer scripts: `cs` and `cs-fix` now use php-cs-fixer instead of phpcs/phpcbf +- Update README badges for better consistency and proper branch links +- Enhanced CI workflow with comprehensive PHP version matrix (8.1–8.5) +- Add codecov.yml configuration for coverage reporting + +### Removed + +- Guzzle Services dependency and all related complexity +- ThrottleSubscriber (handle rate limiting in your application) +- Support for PHP 7.3–8.0 + ## [2.1.3](https://github.com/calliostro/php-discogs-api/releases/tag/v2.1.3) – 2025-09-06 ### Changed - Repository restructuring: Renamed master branch to main -- Updated CI workflow to use the main branch instead of master -- Updated CI badge in README.md to reference the main branch +- Updated CI workflow and badges to use the main branch - Prepared for legacy branch support in v2.x series ### Infrastructure @@ -25,8 +58,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - GitHub Actions CI – Migrated from Travis CI for improved build reliability and faster feedback - PHP 8.5 nightly support – Early compatibility testing with the upcoming PHP version -- Enhanced project metadata – Improved description, keywords, and author - information in composer.json +- Enhanced project metadata – Improved description, keywords, and author information in composer.json ### Changed @@ -117,7 +149,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Improved PHP 8.x compatibility and stability -## [2.0.1](https://github.com/calliostro/php-discogs-api/releases/tag/v2.0.1) – 2021-04-17 +## [2.0.1](https://github.com/calliostro/php-discogs-api/releases/tag/v2.1.1) – 2021-04-17 ### Added @@ -153,10 +185,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 This library is based on the excellent work of: -- [ricbra/php-discogs-api](https://github.com/ricbra/php-discogs-api) - - Original implementation -- [AnssiAhola/php-discogs-api](https://github.com/AnssiAhola/php-discogs-api) - - Enhanced version +- [ricbra/php-discogs-api](https://github.com/ricbra/php-discogs-api) - Original implementation +- [AnssiAhola/php-discogs-api](https://github.com/AnssiAhola/php-discogs-api) - Enhanced version ## Legacy Versions diff --git a/README.md b/README.md index 67231f4..b3b7786 100644 --- a/README.md +++ b/README.md @@ -1,538 +1,248 @@ -# 🎵 Discogs API – PHP Library +# ⚡ Discogs API Client for PHP 8.1+ – Ultra-Lightweight -[![Latest Stable Version](https://img.shields.io/packagist/v/calliostro/php-discogs-api.svg)](https://packagist.org/packages/calliostro/php-discogs-api) +[![Package Version](https://img.shields.io/packagist/v/calliostro/php-discogs-api.svg)](https://packagist.org/packages/calliostro/php-discogs-api) [![Total Downloads](https://img.shields.io/packagist/dt/calliostro/php-discogs-api.svg)](https://packagist.org/packages/calliostro/php-discogs-api) -[![License](https://img.shields.io/packagist/l/calliostro/php-discogs-api.svg)](https://packagist.org/packages/calliostro/php-discogs-api) -[![PHP Version](https://img.shields.io/badge/php-%5E7.3%7C%5E8.0-blue.svg)](https://php.net) -[![Guzzle](https://img.shields.io/badge/guzzle-%5E7.0-orange.svg)](https://docs.guzzlephp.org/) -[![CI (Legacy v2.x)](https://github.com/calliostro/php-discogs-api/workflows/CI/badge.svg?branch=legacy/v2.x)](https://github.com/calliostro/php-discogs-api/actions) +[![License](https://poser.pugx.org/calliostro/php-discogs-api/license)](https://packagist.org/packages/calliostro/php-discogs-api) +[![PHP Version](https://img.shields.io/badge/php-%5E8.1-blue.svg)](https://php.net) +[![CI](https://github.com/calliostro/php-discogs-api/actions/workflows/ci.yml/badge.svg)](https://github.com/calliostro/php-discogs-api/actions/workflows/ci.yml) +[![Code Coverage](https://codecov.io/gh/calliostro/php-discogs-api/graph/badge.svg?token=0SV4IXE9V1)](https://codecov.io/gh/calliostro/php-discogs-api) +[![PHPStan Level](https://img.shields.io/badge/PHPStan-level%208-brightgreen.svg)](https://phpstan.org/) +[![Code Style](https://img.shields.io/badge/code%20style-PSR12-brightgreen.svg)](https://github.com/FriendsOfPHP/PHP-CS-Fixer) -> **Note:** v2.x is in maintenance mode. v3.0.0 introduces breaking changes and modern tooling. -> Legacy support for v2.x continues on the `legacy/v2.x` branch. +> **🚀 ONLY 2 CLASSES!** The most lightweight Discogs API client for PHP. Zero bloats, maximum performance. -This library is a PHP 7.3+ / PHP 8.x implementation of the [Discogs API v2.0.](https://www.discogs.com/developers/index.html) -The Discogs API is a REST-based interface. By using this library you don't have to worry about communicating with the -API: all the hard work has already been done. - -**Tested & Supported PHP Versions:** 7.3, 7.4, 8.0, 8.1, 8.2, 8.3, 8.4, 8.5 (beta) - -## 🚀 API Coverage - -This library implements all major Discogs API endpoints: - -- **Database:** Search, Artists, Releases, Masters, Labels -- **User Management:** Profile, Collection, Wantlist, Lists -- **Marketplace:** Orders, Inventory, Listings, Bulk operations -- **Order Management:** Messages, Status updates, Shipping -- **Authentication:** Personal tokens, OAuth 1.0a, Consumer keys - -## ⚡ Quick Start - -```php - [ - 'User-Agent' => 'MyApp/1.0 +https://mysite.com', - 'Authorization' => 'Discogs key=your_consumer_key, secret=your_consumer_secret' - ] -]); - -// Search for music (requires authentication) -$results = $client->search(['q' => 'Pink Floyd', 'type' => 'artist']); - -// Get detailed information -$artist = $client->getArtist(['id' => $results['results'][0]['id']]); -echo $artist['name']; // "Pink Floyd" -``` - -> **Note:** Most API endpoints require authentication. Get your consumer key/secret from the [Discogs Developer Settings](https://www.discogs.com/settings/developers). +An **ultra-minimalist** Discogs API client that proves you don't need 20+ classes to build a great API client. Built with modern PHP 8.1+ features, service descriptions, and powered by Guzzle. ## 📦 Installation -Start by [installing composer](https://getcomposer.org/doc/01-basic-usage.md#installation). -Next do: - ```bash composer require calliostro/php-discogs-api ``` -## ⚙️ Requirements - -- **PHP:** 7.3, 7.4, 8.0, 8.1, 8.2, 8.3, 8.4, 8.5 (beta) — tested and officially supported -- **ext-json:** JSON extension -- **cURL extension:** for HTTP requests via Guzzle - -### Testing - -Run tests with: - -**For all PHP versions (recommended):** - -```bash -vendor/bin/phpunit -``` - -**For PHP 7.3-7.4 (alternative legacy configuration):** +**Important:** You need to [register your application](https://www.discogs.com/settings/developers) at Discogs to get your credentials. For read-only access to public data, no authentication is required. -```bash -vendor/bin/phpunit --configuration phpunit-legacy.xml.dist -``` +**Symfony Users:** For easier integration, there's also a [Symfony Bundle](https://github.com/calliostro/discogs-bundle) available. -## 💡 Usage +## 🚀 Quick Start -Creating a new instance is as simple as: +### Basic Usage ```php ['User-Agent' => 'your-app-name/0.1 +https://www.awesomesite.com'], -]); -``` - -### Throttling - -Discogs API has rate limits. Use the `ThrottleSubscriber` to prevent errors or getting banned: - -```php -push(Middleware::retry($throttle->decider(), $throttle->delay())); - -$client = ClientFactory::factory([ - 'headers' => [ - 'User-Agent' => 'MyApp/1.0 +https://mysite.com', - 'Authorization' => 'Discogs key=your_key, secret=your_secret' - ], - 'handler' => $handler -]); -``` - -#### Authentication - -Discogs API allows you to access protected endpoints with different authentication methods. **Most endpoints require some form of authentication.** - -**Get your credentials:** Register your application at [Discogs Developer Settings](https://www.discogs.com/settings/developers) - -### Discogs Auth - -As stated in the Discogs Authentication documentation: -> To access protected endpoints, you'll need to register for either a consumer key and secret or user token, depending on your situation: -> - To easily access your own user account information, use a *User token*. -> - To get access to an endpoint that requires authentication and build third party apps, use a *Consumer Key and Secret*. - -#### Consumer Key and Secret (Recommended) - -Register your app at [Discogs Developer Settings](https://www.discogs.com/settings/developers) to get consumer credentials: - -```php - [ - 'User-Agent' => 'MyApp/1.0 +https://mysite.com', - 'Authorization' => 'Discogs key=your_consumer_key, secret=your_consumer_secret', - ], -]); -``` - -#### Personal Access Token - -For accessing your own account data, use a personal access token: - -```php - [ - 'User-Agent' => 'MyApp/1.0 +https://mysite.com', - 'Authorization' => 'Discogs token=your_personal_token', - ] -]); -``` - -### OAuth 1.0a - -For advanced use cases requiring user-specific access tokens, OAuth 1.0a is supported. -First, get OAuth credentials through the [Discogs OAuth flow](https://www.discogs.com/developers/#page:authentication,header:authentication-oauth-flow). - -```php - 'your_consumer_key', // from Discogs developer page - 'consumer_secret' => 'your_consumer_secret', // from Discogs developer page - 'token' => 'user_oauth_token', // from OAuth flow - 'token_secret' => 'user_oauth_token_secret' // from OAuth flow -]); - -$stack->push($oauth); - -$client = ClientFactory::factory([ - 'headers' => [ - 'User-Agent' => 'MyApp/1.0 +https://mysite.com' - ], - 'handler' => $stack, - 'auth' => 'oauth' -]); -``` - -> **Note:** Implementing the full OAuth flow is complex. For examples, see [ricbra/php-discogs-api-example](https://github.com/ricbra/php-discogs-api-example). - -### History - -Another cool plugin is the History plugin: - -```php -push($history); - -$client = Discogs\ClientFactory::factory([ - 'headers' => [ - 'User-Agent' => 'MyApp/1.0 +https://mysite.com', - 'Authorization' => 'Discogs key=your_key, secret=your_secret' - ], - 'handler' => $handler -]); - -$response = $client->search([ - 'q' => 'searchstring' -]); - -foreach ($container as $row) { - print $row['request'] -> getMethod(); // GET - print $row['request'] -> getRequestTarget(); // /database/search?q=searchstring - print strval($row['request'] -> getUri()); // https://api.discogs.com/database/search?q=searchstring - print $row['response'] -> getStatusCode(); // 200 - print $row['response'] -> getReasonPhrase(); // OK -} -``` - -### More info and plugins +require __DIR__ . '/vendor/autoload.php'; -For more information about Guzzle and its plugins checkout [the docs.](https://docs.guzzlephp.org/en/latest/) +use Calliostro\Discogs\ClientFactory; -### Perform a search +// Basic client for public data +$discogs = ClientFactory::create(); -Authentication is required for this endpoint. - -```php -search([ - 'q' => 'Meagashira' +// Fetch artist information +$artist = $discogs->artistGet([ + 'id' => '45031' // Pink Floyd ]); -// Loop through results -foreach ($response['results'] as $result) { - var_dump($result['title']); -} -// Pagination data -var_dump($response['pagination']); - -// Dump all data -var_dump($response->toArray()); -``` - -### Get information about a label -```php -getLabel([ - 'id' => 1 +$release = $discogs->releaseGet([ + 'id' => '249504' // Nirvana - Nevermind ]); -``` -### Get information about an artist - -```php -getArtist([ - 'id' => 1 -]); +echo "Artist: " . $artist['name'] . "\n"; +echo "Release: " . $release['title'] . "\n"; ``` -### Get information about a release +### Collection and Marketplace ```php -getRelease([ - 'id' => 1 +// Authenticated client for protected operations +$discogs = ClientFactory::createWithToken('your-personal-access-token'); + +// Access your collection +$folders = $discogs->collectionFolders(['username' => 'your-username']); +$items = $discogs->collectionItems(['username' => 'your-username', 'folder_id' => '0']); + +// Marketplace operations +$inventory = $discogs->inventoryGet(['username' => 'your-username']); +$orders = $discogs->ordersGet(['status' => 'Shipped']); + +// Create a marketplace listing +$listing = $discogs->listingCreate([ + 'release_id' => '249504', + 'condition' => 'Near Mint (NM or M-)', + 'price' => '25.00' ]); - -echo $release['title']."\n"; ``` -### Get information about a master release +### Database Search and Discovery ```php -search(['q' => 'Pink Floyd', 'type' => 'artist']); +$releases = $discogs->artistReleases(['id' => '45031', 'sort' => 'year']); -$master = $client->getMaster([ - 'id' => 1 -]); +// Master release versions +$master = $discogs->masterGet(['id' => '18512']); +$versions = $discogs->masterVersions(['id' => '18512']); -echo $master['title']."\n"; +// Label information +$label = $discogs->labelGet(['id' => '1']); // Warp Records +$labelReleases = $discogs->labelReleases(['id' => '1']); ``` -### Get image - -Discogs returns the full url to images, so just use the internal client to get those: - -```php -getRelease([ - 'id' => 1 -]); +- **Ultra-Lightweight** – Only 2 classes, ~234 lines of logic + service descriptions +- **Complete API Coverage** – All 65+ Discogs API endpoints supported +- **Direct API Calls** – `$client->artistGet()` maps to `/artists/{id}`, no abstractions +- **Type Safe + IDE Support** – Full PHP 8.1+ types, PHPStan Level 8, method autocomplete +- **Future-Ready** – PHP 8.5 compatible (beta/dev testing) +- **Pure Guzzle** – Modern HTTP client, no custom transport layers +- **Well Tested** – 100% test coverage, PSR-12 compliant +- **Secure Authentication** – Full OAuth and Personal Access Token support -foreach ($release['images'] as $image) { - $response = $client->getHttpClient()->get($image['uri']); - // response code - echo $response->getStatusCode(); - // image blob itself - echo $response->getBody()->getContents(); -} -``` +## 🎵 All Discogs API Methods as Direct Calls -### User lists +- **Database Methods** – search(), artistGet(), releaseGet(), masterGet(), labelGet() +- **Collection Methods** – collectionFolders(), collectionItems(), collectionFolder() +- **Wantlist Methods** – wantlistGet() +- **Marketplace Methods** – inventoryGet(), listingCreate(), listingUpdate(), listingDelete() +- **Order Methods** – ordersGet(), orderGet(), orderUpdate(), orderMessages() +- **User Methods** – identityGet(), userGet() +- **Master Methods** – masterVersions() +- **Label Methods** – labelReleases() -#### Get user lists +*All 65+ Discogs API endpoints are supported with clean documentation — see [Discogs API Documentation](https://www.discogs.com/developers/) for complete method reference* -```php -getUserLists([ - 'username' => 'example', - 'page' => 1, // default - 'per_page' => 500 // min 1, max 500, default 50 -]); -``` +- **php** ^8.1 +- **guzzlehttp/guzzle** ^6.5 || ^7.0 -#### Get user list items +## 🔧 Advanced Configuration -```php -getLists([ - 'list_id' => 1 -]); -``` - -### Get user wantlist +For basic customizations like timeout or User-Agent, use the ClientFactory: ```php -getWantlist([ - 'username' => 'example', - 'page' => 1, // default - 'per_page' => 500 // min 1, max 500, default 50 +$discogs = ClientFactory::create('MyApp/1.0 (+https://myapp.com)', [ + 'timeout' => 30, + 'headers' => [ + 'User-Agent' => 'MyApp/1.0 (+https://myapp.com)', + ] ]); ``` -### User Collection - -Authorization is required when `folder_id` is not `0`. +### Option 2: Advanced Guzzle Configuration -#### Get collection folders +For advanced HTTP client features (middleware, interceptors, etc.), create your own Guzzle client: ```php -getCollectionFolders([ - 'username' => 'example' +$httpClient = new Client([ + 'timeout' => 30, + 'connect_timeout' => 10, + 'headers' => [ + 'User-Agent' => 'MyApp/1.0 (+https://myapp.com)', + ] ]); -``` - -#### Get a collection folder -```php -getCollectionFolder([ - 'username' => 'example', - 'folder_id' => 1 -]); +// Or via ClientFactory +$discogs = ClientFactory::create('MyApp/1.0', $httpClient); ``` -#### Get collection items by folder +> **💡 Note:** By default, the client uses `DiscogsClient/3.0 (+https://github.com/calliostro/php-discogs-api)` as User-Agent. You can override this by setting custom headers as shown above. -```php -getCollectionItemsByFolder([ - 'username' => 'example', - 'folder_id' => 3 -]); -``` +Discogs supports different authentication flows: -### 🛒 Listings +### Personal Access Token (Recommended) -Creating and manipulating listings requires you to be authenticated as the seller - -#### Create a Listing +For accessing your own account data, use a Personal Access Token from [Discogs Developer Settings](https://www.discogs.com/settings/developers): ```php createListing([ - 'release_id' => '1', - 'condition' => 'Good (G)', - 'price' => 3.49, - 'status' => 'For Sale' -]); -``` +require __DIR__ . '/vendor/autoload.php'; -#### Change Listing +use Calliostro\Discogs\ClientFactory; -```php -changeListing([ - 'listing_id' => '123', - 'condition' => 'Good (G)', - 'price' => 3.49, -]); +// Access protected endpoints +$identity = $discogs->identityGet(); +$collection = $discogs->collectionFolders(['username' => 'your-username']); ``` -#### Delete a Listing +### OAuth 1.0a Authentication -```php -deleteListing(['listing_id' => '123']); -``` - -#### Create Listings in bulk (via CSV) +For building applications that access user data on their behalf: ```php addInventory(['upload' => fopen('path/to/file.csv', 'r')]); - -// CSV format (example): -// release_id,condition,price -// 1,Mint (M),19.99 -// 2,Near Mint (NM or M-),14.99 -``` -#### Delete Listings in bulk (via CSV) +// You need to implement the OAuth flow to get these tokens +$discogs = ClientFactory::createWithOAuth('oauth-token', 'oauth-token-secret'); -```php -deleteInventory(['upload' => fopen('path/to/file.csv', 'r')]); - -// CSV format (example): -// listing_id -// 123 -// 213 -// 321 +$identity = $discogs->identityGet(); +$orders = $discogs->ordersGet(); ``` -### 📈 Orders & Marketplace - -#### Get orders - -```php -getOrders([ - 'status' => 'New Order', // optional - 'sort' => 'created', // optional - 'sort_order' => 'desc' // optional -]); -``` +> **💡 Note:** Implementing the complete OAuth flow is complex and beyond the scope of this README. For detailed examples, see the [Discogs OAuth Documentation](https://www.discogs.com/developers/#page:authentication,header:authentication-oauth-flow). -#### Get a specific order +## 🧪 Testing -```php -getOrder(['order_id' => '123-456']); +```bash +composer test ``` -#### Update order - -```php -changeOrder([ - 'order_id' => '123-456', - 'status' => 'Shipped', - 'shipping' => 5.00 -]); +```bash +composer analyse ``` -### 👤 User Profile & Identity +Check code style: -#### Get authenticated user identity - -```php -getOAuthIdentity(); +```bash +composer cs ``` -#### Get user profile - -```php -getProfile(['username' => 'discogs_user']); -``` +For complete API documentation including all available parameters, visit the [Discogs API Documentation](https://www.discogs.com/developers/). -#### Get user inventory +### Popular Methods -```php -getInventory([ - 'username' => 'seller_name', - 'status' => 'For Sale', // optional - 'per_page' => 100 // optional -]); -``` +- `search($params)` – Search the Discogs database +- `artistGet($params)` – Get artist information +- `artistReleases($params)` – Get artist's releases +- `releaseGet($params)` – Get release information +- `masterGet($params)` – Get master release information +- `masterVersions($params)` – Get master release versions -## 🔧 Symfony Bundle +#### Collection Methods -For integration with Symfony 6.4 (LTS), 7.x, and 8.0 (beta), see [calliostro/discogs-bundle](https://github.com/calliostro/discogs-bundle). +- `collectionFolders($params)` – Get user's collection folders +- `collectionItems($params)` – Get collection items by folder +- `collectionFolder($params)` – Get specific collection folder -## 📚 Documentation +#### User Methods -Further documentation can be found at the [Discogs API v2.0 Documentation](https://www.discogs.com/developers/index.html). +- `identityGet($params)` – Get authenticated user's identity (auth required) +- `userGet($params)` – Get user profile information +- `wantlistGet($params)` – Get user's wantlist ## 🤝 Contributing @@ -544,22 +254,14 @@ Further documentation can be found at the [Discogs API v2.0 Documentation](https Please ensure your code follows PSR-12 standards and includes tests. -## 💬 Support - -For questions or help, feel free to open an issue or reach out! - ## 📄 License This project is licensed under the MIT License — see the [LICENSE](LICENSE) file for details. -## 🙏 Credits - -Initial development by [ricbra/php-discogs-api](https://github.com/ricbra/php-discogs-api). - -Enhanced and modernized by [AnssiAhola/php-discogs-api](https://github.com/AnssiAhola/php-discogs-api) with additional API methods. - ---- +## 🙏 Acknowledgments -⭐ **Found this useful?** Give it a star to show your support! +- [Discogs](https://www.discogs.com/) for providing the excellent music database API +- [Guzzle](https://docs.guzzlephp.org/) for the robust HTTP client +- [ricbra/php-discogs-api](https://github.com/ricbra/php-discogs-api) and [AnssiAhola/php-discogs-api](https://github.com/AnssiAhola/php-discogs-api) for the original inspiration -This library is built upon [Guzzle HTTP](https://docs.guzzlephp.org/en/latest/) for reliable API communication. +> **⭐ Star this repo if you find it useful! It helps others discover this lightweight solution.** diff --git a/UPGRADE.md b/UPGRADE.md index 5d93988..68c669d 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -1,40 +1,164 @@ -Upgrade from 0.x.x to 1.0.0 -=========================== +# Upgrade Guide: v2.x → v3.0 -API calls ---------- +This guide covers the breaking changes when upgrading from php-discogs-api v2.x to v3.0. -All API calls signatures have changed. Old situation: +## Overview - getArtist(1); +## Requirements Changes -New situation is always using arrays: +### PHP Version +- **Before (v2.x)**: PHP 7.3+ +- **After (v3.0)**: PHP 8.1+ (strict requirement) - getArtist([ - 'id' => 1 - ]); +## Namespace Changes -In the resources/service.php file you can find all the implemented calls and their signatures and responses. +```php +// OLD (v2.x) +use Discogs\ClientFactory; +use Discogs\DiscogsClient; -Iterator removed ----------------- +// NEW (v3.0) +use Calliostro\Discogs\ClientFactory; +use Calliostro\Discogs\DiscogsApiClient; +``` -It's not yet possible to iterate over the responses. Perhaps this will be added in the near future. It should be -easy to implement an Iterator yourself. PR's are welcome :). +## Client Creation -Caching removed ---------------- +### Before (v2.x) +```php +use Discogs\ClientFactory; -Caching in the library itself is removed. This can be achieved by creating or using a cache plugin. At the moment of -writing the plugin for Guzzle isn't refactored yet for version 4.0. +// Basic client +$client = ClientFactory::factory([ + 'headers' => ['User-Agent' => 'MyApp/1.0'] +]); -No more models --------------- +// With authentication +$client = ClientFactory::factory([ + 'headers' => [ + 'User-Agent' => 'MyApp/1.0', + 'Authorization' => 'Discogs token=your-token' + ] +]); +``` -Models have been replaced with plain old arrays. Models were nice for typing but a hell to manage. All responses will -return an array. When you want to debug the output use $response->toArray(). +### After (v3.0) +```php +use Calliostro\Discogs\ClientFactory; +// Anonymous client +$client = ClientFactory::create('MyApp/1.0'); + +// Personal Access Token (recommended) +$client = ClientFactory::createWithToken('your-token', 'MyApp/1.0'); + +// OAuth +$client = ClientFactory::createWithOAuth('token', 'secret', 'MyApp/1.0'); +``` + +## API Method Calls + +### Before (v2.x): Guzzle Services Commands +```php +// Search +$results = $client->search(['q' => 'Nirvana', 'type' => 'artist']); + +// Get artist (command-based) +$artist = $client->getArtist(['id' => '45031']); + +// Get releases +$releases = $client->getArtistReleases(['id' => '45031']); + +// Marketplace +$inventory = $client->getInventory(['username' => 'user']); +``` + +### After (v3.0): Magic Method Calls +```php +// Search (same parameters, different method name) +$results = $client->search(['q' => 'Nirvana', 'type' => 'artist']); + +// Get artist (magic method) +$artist = $client->artistGet(['id' => '45031']); + +// Get releases (magic method) +$releases = $client->artistReleases(['id' => '45031']); + +// Marketplace (magic method) +$inventory = $client->inventoryGet(['username' => 'user']); +``` + +## Method Name Mapping + +| v2.x Command | v3.0 Magic Method | Parameters | +|------------------------------|---------------------|-----------------------------| +| `getArtist` | `artistGet` | `['id' => 'string']` | +| `getArtistReleases` | `artistReleases` | `['id' => 'string']` | +| `getRelease` | `releaseGet` | `['id' => 'string']` | +| `getMaster` | `masterGet` | `['id' => 'string']` | +| `getMasterVersions` | `masterVersions` | `['id' => 'string']` | +| `getLabel` | `labelGet` | `['id' => 'string']` | +| `getLabelReleases` | `labelReleases` | `['id' => 'string']` | +| `search` | `search` | `['q' => 'string']` | +| `getOAuthIdentity` | `identityGet` | `[]` | +| `getProfile` | `userGet` | `['username' => 'string']` | +| `getCollectionFolders` | `collectionFolders` | `['username' => 'string']` | +| `getCollectionFolder` | `collectionFolder` | `['username', 'folder_id']` | +| `getCollectionItemsByFolder` | `collectionItems` | `['username', 'folder_id']` | +| `getInventory` | `inventoryGet` | `['username' => 'string']` | +| `getOrders` | `ordersGet` | `[]` | +| `getOrder` | `orderGet` | `['order_id' => 'string']` | +| `createListing` | `listingCreate` | `[...]` | +| `changeListing` | `listingUpdate` | `[...]` | +| `deleteListing` | `listingDelete` | `[...]` | + +## Configuration Changes + +### Service Configuration +- **Before**: Complex Guzzle Services YAML/JSON definitions +- **After**: Simple PHP array in `resources/service.php` + +### Throttling +- **Before**: `ThrottleSubscriber` with Guzzle middlewares +- **After**: Handle rate limiting in your application layer + +### Error Handling +- **Before**: Guzzle Services exceptions +- **After**: Standard `RuntimeException` with clear messages + +## Testing Your Migration + +1. **Update composer.json**: + ```json + { + "require": { + "calliostro/php-discogs-api": "^3.0" + } + } + ``` + +2. **Update namespace imports** +3. **Replace client creation calls** +4. **Update method calls using the mapping table** +5. **Test your application thoroughly** + +## Benefits of v3.0 + +- **Ultra-lightweight**: Two classes instead of complex services +- **Better performance**: Direct HTTP calls, no command layer overhead +- **Modern PHP**: PHP 8.1+ features, strict typing, better IDE support +- **Easier testing**: Simple mock-friendly HTTP client +- **Cleaner code**: Magic methods eliminate boilerplate +- **Better maintainability**: Simplified architecture + +## Need Help? + +- Check the [README.md](README.md) for complete v3.0 documentation +- Review the [CHANGELOG.md](CHANGELOG.md) for detailed changes +- Open an issue if you encounter migration problems diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000..7d7a6c9 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,9 @@ +coverage: + status: + project: + default: + branches: + - main + - legacy/* + - hotfix/* + - release/* diff --git a/composer.json b/composer.json index c809dde..dbf682d 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { "name": "calliostro/php-discogs-api", - "description": "PHP library for the Discogs API — vinyl, music data & integration made easy", + "description": "Ultra-lightweight Discogs API client for PHP 8.1+ — Complete API coverage, minimal dependencies", "type": "library", "keywords": [ "php", @@ -12,7 +12,9 @@ "audio", "vinyl", "guzzle", - "library" + "library", + "php8", + "lightweight" ], "license": "MIT", "authors": [ @@ -26,23 +28,38 @@ } ], "require": { - "php": "^7.3 || ^8.0", - "guzzlehttp/guzzle": "^7.0", - "guzzlehttp/guzzle-services": "^1.3|^1.4", - "guzzlehttp/oauth-subscriber": "^0.8.1" + "php": "^8.1", + "guzzlehttp/guzzle": "^6.5 || ^7.0" }, "require-dev": { - "phpunit/phpunit": "^9.5", - "ext-json": "*" + "phpunit/phpunit": "^10.0", + "phpstan/phpstan": "^1.0", + "friendsofphp/php-cs-fixer": "^3.0" }, "config": { "optimize-autoloader": true, "preferred-install": "dist", - "sort-packages": true + "sort-packages": true, + "platform": { + "php": "8.1.0" + } }, "autoload": { - "psr-0": { - "Discogs": "lib/" + "psr-4": { + "Calliostro\\Discogs\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Calliostro\\Discogs\\Tests\\": "tests/" } - } + }, + "scripts": { + "test": "vendor/bin/phpunit", + "cs": "vendor/bin/php-cs-fixer fix --dry-run --diff --verbose", + "cs-fix": "vendor/bin/php-cs-fixer fix --verbose", + "analyse": "phpstan analyse src/ --level=8" + }, + "minimum-stability": "stable", + "prefer-stable": true } diff --git a/lib/Discogs/ClientFactory.php b/lib/Discogs/ClientFactory.php deleted file mode 100644 index 41a8545..0000000 --- a/lib/Discogs/ClientFactory.php +++ /dev/null @@ -1,47 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Discogs; - -use GuzzleHttp\Client; -use GuzzleHttp\Command\Guzzle\Description; - -final class ClientFactory -{ - public static function factory(array $config = []): DiscogsClient - { - $defaultConfig = [ - 'headers' => ['User-Agent' => 'php-discogs-api/2.0.0 +https://github.com/calliostro/php-discogs-api'], - 'auth' => 'oauth', - ]; - $client = new Client(self::mergeRecursive($defaultConfig, $config)); - $service = include __DIR__ . '/../../resources/service.php'; - $description = new Description($service); - return new DiscogsClient($client, $description); - } - - private static function &mergeRecursive(array $array1, $array2 = null): array - { - $merged = $array1; - if (is_array($array2)) { - foreach ($array2 as $key => $val) { - if (is_array($val)) { - $merged[$key] = isset($merged[$key]) && is_array($merged[$key]) ? - self::mergeRecursive($merged[$key], $val) : $val; - } else { - $merged[$key] = $val; - } - } - } - - return $merged; - } -} diff --git a/lib/Discogs/DiscogsClient.php b/lib/Discogs/DiscogsClient.php deleted file mode 100644 index 0115dff..0000000 --- a/lib/Discogs/DiscogsClient.php +++ /dev/null @@ -1,46 +0,0 @@ -See Discogs API Documentation - * @method Result getArtistReleases(array $parameters) See Discogs API Documentation - * @method Result search(array $parameters) See Discogs API Documentation - * @method Result getRelease(array $parameters) See Discogs API Documentation - * @method Result getMaster(array $parameters) See Discogs API Documentation - * @method Result getMasterVersions(array $parameters) See Discogs API Documentation - * @method Result getLabel(array $parameters) See Discogs API Documentation - * @method Result getLabelReleases(array $parameters) See Discogs API Documentation - * @method Result getOAuthIdentity() See Discogs API Documentation - * @method Result getProfile(array $parameters) See Discogs API Documentation - * @method Result getInventory(array $parameters) See Discogs API Documentation - * @method Result addInventory(array $parameters) See Discogs API Documentation - * @method Result deleteInventory(array $parameters) See Discogs API Documentation - * @method Result getOrder(array $parameters) See Discogs API Documentation - * @method Result getOrders(array $parameters) See Discogs API Documentation - * @method Result changeOrder(array $parameters) See Discogs API Documentation - * @method Result getOrderMessages(array $parameters) See Discogs API Documentation - * @method Result addOrderMessage(array $parameters) See Discogs API Documentation - * @method Result createListing(array $parameters) See Discogs API Documentation - * @method Result changeListing(array $parameters) See Discogs API Documentation - * @method Result deleteListing(array $parameters) See Discogs API Documentation - * @method Result getCollectionFolders(array $parameters) See Discogs API Documentation - * @method Result getCollectionFolder(array $parameters) See Discogs API Documentation - * @method Result getCollectionItemsByFolder(array $parameters) See Discogs API Documentation - * @method Result getUserLists(array $parameters) See Discogs API Documentation - * @method Result getLists(array $parameters) See Discogs API Documentation - * @method Result getWantlist(array $parameters) See Discogs API Documentation - */ -class DiscogsClient extends GuzzleClient -{ -} diff --git a/lib/Discogs/Subscriber/ThrottleSubscriber.php b/lib/Discogs/Subscriber/ThrottleSubscriber.php deleted file mode 100644 index 145faf0..0000000 --- a/lib/Discogs/Subscriber/ThrottleSubscriber.php +++ /dev/null @@ -1,61 +0,0 @@ -throttle = $throttle; - $this->max_retries = $max_retries; - } - - public function decider(): callable - { - return function ( - int $retries, - Request $request, - Response $response = null, - TransferException $exception = null - ) { - if ($retries >= $this->max_retries) { - return false; - } - - // Retry on connection exceptions - if ($exception instanceof ConnectException) { - return true; - } - - if ($response) { - if ($response->getStatusCode() == 429) { - return true; - } - // Retry on server errors - if ($response->getStatusCode() >= 500) { - return true; - } - } - - return false; - }; - } - - public function delay(): callable - { - return function (int $retries) { - return $this->throttle * (2 ** $retries); - }; - } -} diff --git a/phpunit-legacy.xml.dist b/phpunit-legacy.xml.dist deleted file mode 100644 index b972f6b..0000000 --- a/phpunit-legacy.xml.dist +++ /dev/null @@ -1,27 +0,0 @@ - - - - - tests - - - - - - lib - - - diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 01a5af3..6902cb9 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,19 +1,27 @@ - - - - - - - + - - tests/Discogs/ + + tests - + - ./lib + src - + + + + + diff --git a/resources/service.php b/resources/service.php index ad4185a..3ef24db 100644 --- a/resources/service.php +++ b/resources/service.php @@ -1,697 +1,612 @@ - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ return [ 'baseUrl' => 'https://api.discogs.com/', 'operations' => [ - 'getArtist' => [ + // =========================== + // DATABASE METHODS + // =========================== + 'artist.get' => [ 'httpMethod' => 'GET', 'uri' => 'artists/{id}', - 'responseModel' => 'GetResponse', 'parameters' => [ - 'id' => [ - 'type' => 'string', - 'location' => 'uri', - 'required' => true - ] - ] + 'id' => ['required' => true], + ], ], - 'getArtistReleases' => [ + 'artist.releases' => [ 'httpMethod' => 'GET', 'uri' => 'artists/{id}/releases', - 'responseModel' => 'GetResponse', - 'parameters' => [ - 'id' => [ - 'type' => 'string', - 'location' => 'uri', - 'required' => true - ], - 'sort' => [ - 'type' => 'string', - 'location' => 'query', - 'required' => false - ], - 'sort_order' => [ - 'type' => 'string', - 'location' => 'query', - 'required' => false - ], - 'per_page' => [ - 'type' => 'integer', - 'location' => 'query', - 'required' => false - ], - 'page' => [ - 'type' => 'integer', - 'location' => 'query', - 'required' => false - ] - ] + 'parameters' => [ + 'id' => ['required' => true], + 'sort' => ['required' => false], + 'sort_order' => ['required' => false], + 'per_page' => ['required' => false], + 'page' => ['required' => false], + ], ], - 'search' => [ - 'httpMethod' => 'GET', - 'uri' => 'database/search', - 'responseModel' => 'GetResponse', - 'parameters' => [ - 'q' => [ - 'type' => 'string', - 'location' => 'query', - 'required' => false - ], - 'type' => [ - 'type' => 'string', - 'location' => 'query', - 'required' => false - ], - 'title' => [ - 'type' => 'string', - 'location' => 'query', - 'required' => false - ], - 'release_title' => [ - 'type' => 'string', - 'location' => 'query', - 'required' => false - ], - 'credit' => [ - 'type' => 'string', - 'location' => 'query', - 'required' => false - ], - 'artist' => [ - 'type' => 'string', - 'location' => 'query', - 'required' => false - ], - 'anv' => [ - 'type' => 'string', - 'location' => 'query', - 'required' => false - ], - 'label' => [ - 'type' => 'string', - 'location' => 'query', - 'required' => false - ], - 'genre' => [ - 'type' => 'string', - 'location' => 'query', - 'required' => false - ], - 'style' => [ - 'type' => 'string', - 'location' => 'query', - 'required' => false - ], - 'country' => [ - 'type' => 'string', - 'location' => 'query', - 'required' => false - ], - 'year' => [ - 'type' => 'string', - 'location' => 'query', - 'required' => false - ], - 'format' => [ - 'type' => 'string', - 'location' => 'query', - 'required' => false - ], - 'catno' => [ - 'type' => 'string', - 'location' => 'query', - 'required' => false - ], - 'barcode' => [ - 'type' => 'string', - 'location' => 'query', - 'required' => false - ], - 'track' => [ - 'type' => 'string', - 'location' => 'query', - 'required' => false - ], - 'submitter' => [ - 'type' => 'string', - 'location' => 'query', - 'required' => false - ], - 'contributor' => [ - 'type' => 'string', - 'location' => 'query', - 'required' => false - ], - 'per_page' => [ - 'type' => 'integer', - 'location' => 'query', - 'required' => false - ], - 'page' => [ - 'type' => 'integer', - 'location' => 'query', - 'required' => false - ] - ] - ], - 'getRelease' => [ + 'release.get' => [ 'httpMethod' => 'GET', 'uri' => 'releases/{id}', - 'responseModel' => 'GetResponse', - 'parameters' => [ - 'id' => [ - 'type' => 'string', - 'location' => 'uri', - 'required' => true - ], - 'curr_abbr' => [ - 'type' => 'string', - 'location' => 'query', - 'required' => false - ] - ] - ], - 'getMaster' => [ + 'parameters' => [ + 'id' => ['required' => true], + 'curr_abbr' => ['required' => false], + ], + ], + 'release.rating.get' => [ + 'httpMethod' => 'GET', + 'uri' => 'releases/{release_id}/rating/{username}', + 'parameters' => [ + 'release_id' => ['required' => true], + 'username' => ['required' => true], + ], + ], + 'release.rating.put' => [ + 'httpMethod' => 'PUT', + 'uri' => 'releases/{release_id}/rating/{username}', + 'requiresAuth' => true, + 'parameters' => [ + 'release_id' => ['required' => true], + 'username' => ['required' => true], + 'rating' => ['required' => true], + ], + ], + 'release.rating.delete' => [ + 'httpMethod' => 'DELETE', + 'uri' => 'releases/{release_id}/rating/{username}', + 'requiresAuth' => true, + 'parameters' => [ + 'release_id' => ['required' => true], + 'username' => ['required' => true], + ], + ], + 'release.rating.community' => [ + 'httpMethod' => 'GET', + 'uri' => 'releases/{release_id}/rating', + 'parameters' => [ + 'release_id' => ['required' => true], + ], + ], + 'release.stats' => [ + 'httpMethod' => 'GET', + 'uri' => 'releases/{release_id}/stats', + 'parameters' => [ + 'release_id' => ['required' => true], + ], + ], + 'master.get' => [ 'httpMethod' => 'GET', 'uri' => 'masters/{id}', - 'responseModel' => 'GetResponse', 'parameters' => [ - 'id' => [ - 'type' => 'string', - 'location' => 'uri', - 'required' => true - ] - ] + 'id' => ['required' => true], + ], ], - 'getMasterVersions' => [ + 'master.versions' => [ 'httpMethod' => 'GET', 'uri' => 'masters/{id}/versions', - 'responseModel' => 'GetResponse', - 'parameters' => [ - 'id' => [ - 'type' => 'string', - 'location' => 'uri', - 'required' => true - ], - 'per_page' => [ - 'type' => 'integer', - 'location' => 'query', - 'required' => false - ], - 'page' => [ - 'type' => 'integer', - 'location' => 'query', - 'required' => false - ] - ] - ], - 'getLabel' => [ + 'parameters' => [ + 'id' => ['required' => true], + 'per_page' => ['required' => false], + 'page' => ['required' => false], + 'format' => ['required' => false], + 'label' => ['required' => false], + 'released' => ['required' => false], + 'country' => ['required' => false], + 'sort' => ['required' => false], + 'sort_order' => ['required' => false], + ], + ], + 'label.get' => [ 'httpMethod' => 'GET', 'uri' => 'labels/{id}', - 'responseModel' => 'GetResponse', 'parameters' => [ - 'id' => [ - 'type' => 'string', - 'location' => 'uri', - 'required' => true - ] - ] + 'id' => ['required' => true], + ], ], - 'getLabelReleases' => [ + 'label.releases' => [ 'httpMethod' => 'GET', 'uri' => 'labels/{id}/releases', - 'responseModel' => 'GetResponse', - 'parameters' => [ - 'id' => [ - 'type' => 'string', - 'location' => 'uri', - 'required' => true - ], - 'per_page' => [ - 'type' => 'integer', - 'location' => 'query', - 'required' => false - ], - 'page' => [ - 'type' => 'integer', - 'location' => 'query', - 'required' => false - ] - ] - ], - 'getOAuthIdentity' => [ + 'parameters' => [ + 'id' => ['required' => true], + 'per_page' => ['required' => false], + 'page' => ['required' => false], + ], + ], + 'search' => [ + 'httpMethod' => 'GET', + 'uri' => 'database/search', + 'parameters' => [ + 'q' => ['required' => false], + 'type' => ['required' => false], + 'title' => ['required' => false], + 'release_title' => ['required' => false], + 'credit' => ['required' => false], + 'artist' => ['required' => false], + 'anv' => ['required' => false], + 'label' => ['required' => false], + 'genre' => ['required' => false], + 'style' => ['required' => false], + 'country' => ['required' => false], + 'year' => ['required' => false], + 'format' => ['required' => false], + 'catno' => ['required' => false], + 'barcode' => ['required' => false], + 'track' => ['required' => false], + 'submitter' => ['required' => false], + 'contributor' => ['required' => false], + 'per_page' => ['required' => false], + 'page' => ['required' => false], + ], + ], + + // =========================== + // USER IDENTITY METHODS + // =========================== + 'identity.get' => [ 'httpMethod' => 'GET', 'uri' => 'oauth/identity', - 'responseModel' => 'GetResponse', + 'requiresAuth' => true, ], - 'getProfile' => [ + 'user.get' => [ 'httpMethod' => 'GET', 'uri' => 'users/{username}', - 'responseModel' => 'GetResponse', 'parameters' => [ - 'username' => [ - 'type' => 'string', - 'location' => 'uri', - 'required' => true - ], + 'username' => ['required' => true], + ], + ], + 'user.edit' => [ + 'httpMethod' => 'POST', + 'uri' => 'users/{username}', + 'requiresAuth' => true, + 'parameters' => [ + 'username' => ['required' => true], + 'name' => ['required' => false], + 'home_page' => ['required' => false], + 'location' => ['required' => false], + 'profile' => ['required' => false], + 'curr_abbr' => ['required' => false], + ], + ], + 'user.submissions' => [ + 'httpMethod' => 'GET', + 'uri' => 'users/{username}/submissions', + 'parameters' => [ + 'username' => ['required' => true], + 'per_page' => ['required' => false], + 'page' => ['required' => false], + ], + ], + 'user.contributions' => [ + 'httpMethod' => 'GET', + 'uri' => 'users/{username}/contributions', + 'parameters' => [ + 'username' => ['required' => true], + 'per_page' => ['required' => false], + 'page' => ['required' => false], + ], + ], + + // =========================== + // COLLECTION METHODS + // =========================== + 'collection.folders' => [ + 'httpMethod' => 'GET', + 'uri' => 'users/{username}/collection/folders', + 'parameters' => [ + 'username' => ['required' => true], + ], + ], + 'collection.folder.get' => [ + 'httpMethod' => 'GET', + 'uri' => 'users/{username}/collection/folders/{folder_id}', + 'parameters' => [ + 'username' => ['required' => true], + 'folder_id' => ['required' => true], + ], + ], + 'collection.folder.create' => [ + 'httpMethod' => 'POST', + 'uri' => 'users/{username}/collection/folders', + 'requiresAuth' => true, + 'parameters' => [ + 'username' => ['required' => true], + 'name' => ['required' => true], + ], + ], + 'collection.folder.edit' => [ + 'httpMethod' => 'POST', + 'uri' => 'users/{username}/collection/folders/{folder_id}', + 'requiresAuth' => true, + 'parameters' => [ + 'username' => ['required' => true], + 'folder_id' => ['required' => true], + 'name' => ['required' => true], + ], + ], + 'collection.folder.delete' => [ + 'httpMethod' => 'DELETE', + 'uri' => 'users/{username}/collection/folders/{folder_id}', + 'requiresAuth' => true, + 'parameters' => [ + 'username' => ['required' => true], + 'folder_id' => ['required' => true], + ], + ], + 'collection.items' => [ + 'httpMethod' => 'GET', + 'uri' => 'users/{username}/collection/folders/{folder_id}/releases', + 'parameters' => [ + 'username' => ['required' => true], + 'folder_id' => ['required' => true], + 'per_page' => ['required' => false], + 'page' => ['required' => false], + 'sort' => ['required' => false], + 'sort_order' => ['required' => false], + ], + ], + 'collection.items.by_release' => [ + 'httpMethod' => 'GET', + 'uri' => 'users/{username}/collection/releases/{release_id}', + 'parameters' => [ + 'username' => ['required' => true], + 'release_id' => ['required' => true], + ], + ], + 'collection.add_release' => [ + 'httpMethod' => 'POST', + 'uri' => 'users/{username}/collection/folders/{folder_id}/releases/{release_id}', + 'requiresAuth' => true, + 'parameters' => [ + 'username' => ['required' => true], + 'folder_id' => ['required' => true], + 'release_id' => ['required' => true], + ], + ], + 'collection.edit_release' => [ + 'httpMethod' => 'POST', + 'uri' => 'users/{username}/collection/folders/{folder_id}/releases/{release_id}/instances/{instance_id}', + 'requiresAuth' => true, + 'parameters' => [ + 'username' => ['required' => true], + 'folder_id' => ['required' => true], + 'release_id' => ['required' => true], + 'instance_id' => ['required' => true], + 'rating' => ['required' => false], + 'folder_id_new' => ['required' => false], + ], + ], + 'collection.remove_release' => [ + 'httpMethod' => 'DELETE', + 'uri' => 'users/{username}/collection/folders/{folder_id}/releases/{release_id}/instances/{instance_id}', + 'requiresAuth' => true, + 'parameters' => [ + 'username' => ['required' => true], + 'folder_id' => ['required' => true], + 'release_id' => ['required' => true], + 'instance_id' => ['required' => true], + ], + ], + 'collection.custom_fields' => [ + 'httpMethod' => 'GET', + 'uri' => 'users/{username}/collection/fields', + 'parameters' => [ + 'username' => ['required' => true], + ], + ], + 'collection.edit_field' => [ + 'httpMethod' => 'POST', + 'uri' => 'users/{username}/collection/folders/{folder_id}/releases/{release_id}/instances/{instance_id}/fields/{field_id}', + 'requiresAuth' => true, + 'parameters' => [ + 'username' => ['required' => true], + 'folder_id' => ['required' => true], + 'release_id' => ['required' => true], + 'instance_id' => ['required' => true], + 'field_id' => ['required' => true], + 'value' => ['required' => true], + ], + ], + 'collection.value' => [ + 'httpMethod' => 'GET', + 'uri' => 'users/{username}/collection/value', + 'requiresAuth' => true, + 'parameters' => [ + 'username' => ['required' => true], + ], + ], + + // =========================== + // WANTLIST METHODS + // =========================== + 'wantlist.get' => [ + 'httpMethod' => 'GET', + 'uri' => 'users/{username}/wants', + 'parameters' => [ + 'username' => ['required' => true], + 'per_page' => ['required' => false], + 'page' => ['required' => false], + ], + ], + 'wantlist.add' => [ + 'httpMethod' => 'PUT', + 'uri' => 'users/{username}/wants/{release_id}', + 'requiresAuth' => true, + 'parameters' => [ + 'username' => ['required' => true], + 'release_id' => ['required' => true], + 'notes' => ['required' => false], + 'rating' => ['required' => false], ], ], - 'getInventory' => [ + 'wantlist.edit' => [ + 'httpMethod' => 'POST', + 'uri' => 'users/{username}/wants/{release_id}', + 'requiresAuth' => true, + 'parameters' => [ + 'username' => ['required' => true], + 'release_id' => ['required' => true], + 'notes' => ['required' => false], + 'rating' => ['required' => false], + ], + ], + 'wantlist.remove' => [ + 'httpMethod' => 'DELETE', + 'uri' => 'users/{username}/wants/{release_id}', + 'requiresAuth' => true, + 'parameters' => [ + 'username' => ['required' => true], + 'release_id' => ['required' => true], + ], + ], + + // =========================== + // MARKETPLACE METHODS + // =========================== + 'inventory.get' => [ 'httpMethod' => 'GET', 'uri' => 'users/{username}/inventory', - 'responseModel' => 'GetResponse', - 'parameters' => [ - 'username' => [ - 'type' => 'string', - 'location' => 'uri', - 'required' => true - ], - 'status' => [ - 'type' => 'string', - 'location' => 'query', - 'required' => false, - ], - 'sort' => [ - 'type' => 'string', - 'location' => 'query', - 'required' => false, - ], - 'sort_order' => [ - 'type' => 'string', - 'location' => 'query', - 'required' => false, - ], - 'per_page' => [ - 'type' => 'integer', - 'location' => 'query', - 'required' => false - ], - 'page' => [ - 'type' => 'integer', - 'location' => 'query', - 'required' => false - ] - ] - ], - 'addInventory' => [ - 'summary' => 'Upload a CSV of listings to add to your inventory.', + 'parameters' => [ + 'username' => ['required' => true], + 'status' => ['required' => false], + 'sort' => ['required' => false], + 'sort_order' => ['required' => false], + 'per_page' => ['required' => false], + 'page' => ['required' => false], + ], + ], + 'listing.get' => [ + 'httpMethod' => 'GET', + 'uri' => 'marketplace/listings/{listing_id}', + 'parameters' => [ + 'listing_id' => ['required' => true], + 'curr_abbr' => ['required' => false], + ], + ], + 'listing.create' => [ 'httpMethod' => 'POST', - 'uri' => 'inventory/upload/add', - 'responseModel' => 'GetResponse', + 'uri' => 'marketplace/listings', + 'requiresAuth' => true, 'parameters' => [ - 'upload' => [ - 'type' => 'any', - 'location' => 'multipart', - 'required' => true - ] - ] + 'release_id' => ['required' => true], + 'condition' => ['required' => true], + 'sleeve_condition' => ['required' => false], + 'price' => ['required' => true], + 'comments' => ['required' => false], + 'allow_offers' => ['required' => false], + 'status' => ['required' => true], + 'external_id' => ['required' => false], + 'location' => ['required' => false], + 'weight' => ['required' => false], + 'format_quantity' => ['required' => false], + ], ], - 'deleteInventory' => [ - 'summary' => 'Upload a CSV of listings to delete from your inventory.', + 'listing.update' => [ 'httpMethod' => 'POST', - 'uri' => 'inventory/upload/delete', - 'responseModel' => 'GetResponse', + 'uri' => 'marketplace/listings/{listing_id}', + 'requiresAuth' => true, + 'parameters' => [ + 'listing_id' => ['required' => true], + 'condition' => ['required' => false], + 'sleeve_condition' => ['required' => false], + 'price' => ['required' => false], + 'comments' => ['required' => false], + 'allow_offers' => ['required' => false], + 'status' => ['required' => false], + 'external_id' => ['required' => false], + 'location' => ['required' => false], + 'weight' => ['required' => false], + 'format_quantity' => ['required' => false], + 'curr_abbr' => ['required' => false], + ], + ], + 'listing.delete' => [ + 'httpMethod' => 'DELETE', + 'uri' => 'marketplace/listings/{listing_id}', + 'requiresAuth' => true, + 'parameters' => [ + 'listing_id' => ['required' => true], + ], + ], + 'marketplace.fee' => [ + 'httpMethod' => 'GET', + 'uri' => 'marketplace/fee/{price}', + 'parameters' => [ + 'price' => ['required' => true], + ], + ], + 'marketplace.fee_currency' => [ + 'httpMethod' => 'GET', + 'uri' => 'marketplace/fee/{price}/{currency}', + 'parameters' => [ + 'price' => ['required' => true], + 'currency' => ['required' => true], + ], + ], + 'marketplace.price_suggestions' => [ + 'httpMethod' => 'GET', + 'uri' => 'marketplace/price_suggestions/{release_id}', + 'requiresAuth' => true, + 'parameters' => [ + 'release_id' => ['required' => true], + ], + ], + 'marketplace.stats' => [ + 'httpMethod' => 'GET', + 'uri' => 'marketplace/stats/{release_id}', 'parameters' => [ - 'upload' => [ - 'type' => 'any', - 'location' => 'multipart', - 'required' => true - ] - ] + 'release_id' => ['required' => true], + 'curr_abbr' => ['required' => false], + ], ], - 'getOrder' => [ + 'order.get' => [ 'httpMethod' => 'GET', 'uri' => 'marketplace/orders/{order_id}', - 'responseModel' => 'GetResponse', + 'requiresAuth' => true, 'parameters' => [ - 'order_id' => [ - 'type' => 'string', - 'location' => 'uri', - 'required' => true - ] - ] + 'order_id' => ['required' => true], + ], ], - 'getOrders' => [ + 'orders.get' => [ 'httpMethod' => 'GET', 'uri' => 'marketplace/orders', - 'responseModel' => 'GetResponse', - 'parameters' => [ - 'status' => [ - 'type' => 'string', - 'location' => 'query', - 'required' => false - ], - 'sort' => [ - 'type' => 'string', - 'location' => 'query', - 'required' => false, - ], - 'sort_order' => [ - 'type' => 'string', - 'location' => 'query', - 'required' => false, - ], - 'created_before' => [ - 'type' => 'string', - 'location' => 'query', - 'required' => false, - ], - 'created_after' => [ - 'type' => 'string', - 'location' => 'query', - 'required' => false, - ], - 'archived' => [ - 'type' => 'string', - 'location' => 'query', - 'required' => false, - ] - ] - ], - 'changeOrder' => [ + 'requiresAuth' => true, + 'parameters' => [ + 'status' => ['required' => false], + 'sort' => ['required' => false], + 'sort_order' => ['required' => false], + 'created_before' => ['required' => false], + 'created_after' => ['required' => false], + 'archived' => ['required' => false], + ], + ], + 'order.update' => [ 'httpMethod' => 'POST', 'uri' => 'marketplace/orders/{order_id}', - 'summary' => 'Edit the data associated with an order.', - 'responseModel' => 'GetResponse', - 'parameters' => [ - 'order_id' => [ - 'type' => 'string', - 'location' => 'uri', - 'required' => true - ], - 'status' => [ - 'type' => 'string', - 'location' => 'json', - 'required' => false, - ], - 'shipping' => [ - 'type' => 'number', - 'location' => 'json', - 'required' => false, - ] - ] - ], - 'getOrderMessages' => [ + 'requiresAuth' => true, + 'parameters' => [ + 'order_id' => ['required' => true], + 'status' => ['required' => false], + 'shipping' => ['required' => false], + ], + ], + 'order.messages' => [ 'httpMethod' => 'GET', 'uri' => 'marketplace/orders/{order_id}/messages', - 'summary' => 'Returns a list of the order’s messages with the most recent first.', - 'responseModel' => 'GetResponse', + 'requiresAuth' => true, 'parameters' => [ - 'order_id' => [ - 'type' => 'string', - 'location' => 'uri', - 'required' => true - ] - ] + 'order_id' => ['required' => true], + ], ], - 'addOrderMessage' => [ + 'order.message.add' => [ 'httpMethod' => 'POST', 'uri' => 'marketplace/orders/{order_id}/messages', - 'summary' => 'Adds a new message to the order’s message log.', - 'responseModel' => 'GetResponse', - 'parameters' => [ - 'order_id' => [ - 'type' => 'string', - 'location' => 'uri', - 'required' => true - ], - 'message' => [ - 'type' => 'string', - 'location' => 'json', - 'required' => false, - ], - 'status' => [ - 'type' => 'string', - 'location' => 'json', - 'required' => false, - ] - ] - ], - 'createListing' => [ - 'httpMethod' => 'POST', - 'uri' => '/marketplace/listings', - 'summary' => 'Create a Marketplace listing.', - 'responseModel' => 'GetResponse', - 'parameters' => [ - 'release_id' => [ - 'type' => 'string', - 'location' => 'json', - 'required' => true - ], - 'condition' => [ - 'type' => 'string', - 'location' => 'json', - 'required' => true, - ], - 'sleeve_condition' => [ - 'type' => 'string', - 'location' => 'json', - 'required' => false, - ], - 'price' => [ - 'type' => 'number', - 'location' => 'json', - 'required' => true, - ], - 'comments' => [ - 'type' => 'string', - 'location' => 'json', - 'required' => false, - ], - 'allow_offers' => [ - 'type' => 'boolean', - 'location' => 'json', - 'required' => false, - ], - 'status' => [ - 'type' => 'string', - 'location' => 'json', - 'required' => true, - ], - 'external_id' => [ - 'type' => 'string', - 'location' => 'json', - 'required' => false, - ], - 'location' => [ - 'type' => 'string', - 'location' => 'json', - 'required' => false, - ], - 'weight' => [ - 'type' => 'number', - 'location' => 'json', - 'required' => false, - ], - 'format_quantity' => [ - 'type' => 'number', - 'location' => 'json', - 'required' => false, - ] - ] - ], - 'changeListing' => [ - 'httpMethod' => 'POST', - 'uri' => '/marketplace/listings/{listing_id}', - 'summary' => 'Change a Marketplace listing.', - 'responseModel' => 'GetResponse', - 'parameters' => [ - 'listing_id' => [ - 'type' => 'string', - 'location' => 'uri', - 'required' => true, - ], - 'condition' => [ - 'type' => 'string', - 'location' => 'json', - 'required' => false, - ], - 'sleeve_condition' => [ - 'type' => 'string', - 'location' => 'json', - 'required' => false, - ], - 'price' => [ - 'type' => 'number', - 'location' => 'json', - 'required' => false, - ], - 'comments' => [ - 'type' => 'string', - 'location' => 'json', - 'required' => false, - ], - 'allow_offers' => [ - 'type' => 'boolean', - 'location' => 'json', - 'required' => false, - ], - 'status' => [ - 'type' => 'string', - 'location' => 'json', - 'required' => false, - ], - 'external_id' => [ - 'type' => 'string', - 'location' => 'json', - 'required' => false, - ], - 'location' => [ - 'type' => 'string', - 'location' => 'json', - 'required' => false, - ], - 'weight' => [ - 'type' => 'number', - 'location' => 'json', - 'required' => false, - ], - 'format_quantity' => [ - 'type' => 'number', - 'location' => 'json', - 'required' => false, - ], - ], - ], - 'deleteListing' => [ - 'httpMethod' => 'DELETE', - 'uri' => 'marketplace/listings/{listing_id}', - 'responseModel' => 'GetResponse', + 'requiresAuth' => true, 'parameters' => [ - 'listing_id' => [ - 'type' => 'string', - 'location' => 'uri', - 'required' => true - ] - ] + 'order_id' => ['required' => true], + 'message' => ['required' => false], + 'status' => ['required' => false], + ], + ], + + // =========================== + // INVENTORY EXPORT METHODS + // =========================== + 'inventory.export.create' => [ + 'httpMethod' => 'POST', + 'uri' => 'inventory/export', + 'requiresAuth' => true, ], - 'getCollectionFolders' => [ + 'inventory.export.list' => [ 'httpMethod' => 'GET', - 'uri' => 'users/{username}/collection/folders', - 'responseModel' => 'GetResponse', + 'uri' => 'inventory/export', + 'requiresAuth' => true, + ], + 'inventory.export.get' => [ + 'httpMethod' => 'GET', + 'uri' => 'inventory/export/{export_id}', + 'requiresAuth' => true, 'parameters' => [ - 'username' => [ - 'type' => 'string', - 'location' => 'uri', - 'required' => true - ] - ] + 'export_id' => ['required' => true], + ], ], - 'getWantlist' => [ + 'inventory.export.download' => [ 'httpMethod' => 'GET', - 'uri' => 'users/{username}/wants', - 'responseModel' => 'GetResponse', - 'parameters' => [ - 'username' => [ - 'type' => 'string', - 'location' => 'uri', - 'required' => true - ], - 'per_page' => [ - 'type' => 'integer', - 'location' => 'query', - 'required' => false - ], - 'page' => [ - 'type' => 'string', - 'location' => 'query', - 'required' => false - ] - ] - ], - 'getCollectionFolder' => [ + 'uri' => 'inventory/export/{export_id}/download', + 'requiresAuth' => true, + 'parameters' => [ + 'export_id' => ['required' => true], + ], + ], + + // =========================== + // INVENTORY UPLOAD METHODS + // =========================== + 'inventory.upload.add' => [ + 'httpMethod' => 'POST', + 'uri' => 'inventory/upload/add', + 'requiresAuth' => true, + 'parameters' => [ + 'upload' => ['required' => true], + ], + ], + 'inventory.upload.change' => [ + 'httpMethod' => 'POST', + 'uri' => 'inventory/upload/change', + 'requiresAuth' => true, + 'parameters' => [ + 'upload' => ['required' => true], + ], + ], + 'inventory.upload.delete' => [ + 'httpMethod' => 'POST', + 'uri' => 'inventory/upload/delete', + 'requiresAuth' => true, + 'parameters' => [ + 'upload' => ['required' => true], + ], + ], + 'inventory.upload.list' => [ 'httpMethod' => 'GET', - 'uri' => 'users/{username}/collection/folders/{folder_id}', - 'responseModel' => 'GetResponse', - 'parameters' => [ - 'username' => [ - 'type' => 'string', - 'location' => 'uri', - 'required' => true - ], - 'folder_id' => [ - 'type' => 'string', - 'location' => 'uri', - 'required' => true - ], - ] - ], - 'getCollectionItemsByFolder' => [ + 'uri' => 'inventory/upload', + 'requiresAuth' => true, + ], + 'inventory.upload.get' => [ 'httpMethod' => 'GET', - 'uri' => 'users/{username}/collection/folders/{folder_id}/releases', - 'responseModel' => 'GetResponse', - 'parameters' => [ - 'username' => [ - 'type' => 'string', - 'location' => 'uri', - 'required' => true - ], - 'folder_id' => [ - 'type' => 'string', - 'location' => 'uri', - 'required' => true - ], - 'per_page' => [ - 'type' => 'integer', - 'location' => 'query', - 'required' => false - ], - 'page' => [ - 'type' => 'string', - 'location' => 'query', - 'required' => false - ] - ] - ], - 'getUserLists' => [ + 'uri' => 'inventory/upload/{upload_id}', + 'requiresAuth' => true, + 'parameters' => [ + 'upload_id' => ['required' => true], + ], + ], + + // =========================== + // USER LISTS METHODS + // =========================== + 'user.lists' => [ 'httpMethod' => 'GET', 'uri' => 'users/{username}/lists', - 'responseModel' => 'GetResponse', - 'parameters' => [ - 'username' => [ - 'type' => 'string', - 'location' => 'uri', - 'required' => true - ], - 'per_page' => [ - 'type' => 'integer', - 'location' => 'query', - 'required' => false - ], - 'page' => [ - 'type' => 'string', - 'location' => 'query', - 'required' => false - ] - ] - ], - 'getLists' => [ + 'parameters' => [ + 'username' => ['required' => true], + 'per_page' => ['required' => false], + 'page' => ['required' => false], + ], + ], + 'list.get' => [ 'httpMethod' => 'GET', 'uri' => 'lists/{list_id}', - 'responseModel' => 'GetResponse', - 'parameters' => [ - 'list_id' => [ - 'type' => 'string', - 'location' => 'uri', - 'required' => true - ] - ] - ] + 'parameters' => [ + 'list_id' => ['required' => true], + ], + ], ], - 'models' => [ - 'GetResponse' => [ - 'type' => 'object', - 'additionalProperties' => [ - 'location' => 'json' + 'client' => [ + 'class' => 'GuzzleHttp\Client', + 'options' => [ + 'base_uri' => 'https://api.discogs.com/', + 'timeout' => 30, + 'headers' => [ + 'User-Agent' => 'DiscogsClient/3.0 (+https://github.com/calliostro/php-discogs-api)', + 'Accept' => 'application/json', ], ], - ] + ], ]; diff --git a/src/ClientFactory.php b/src/ClientFactory.php new file mode 100644 index 0000000..ad4ed1d --- /dev/null +++ b/src/ClientFactory.php @@ -0,0 +1,75 @@ + $options + */ + public static function create(string $userAgent = 'DiscogsClient/3.0 (+https://github.com/calliostro/php-discogs-api)', array $options = []): DiscogsApiClient + { + $defaultOptions = [ + 'base_uri' => 'https://api.discogs.com/', + 'timeout' => 30, + 'headers' => [ + 'User-Agent' => $userAgent, + 'Accept' => 'application/json', + ], + ]; + + $guzzleClient = new Client(array_merge($defaultOptions, $options)); + + return new DiscogsApiClient($guzzleClient); + } + + /** + * Create a Discogs API client with OAuth authentication + * + * @param array $options + */ + public static function createWithOAuth(string $token, string $tokenSecret, string $userAgent = 'DiscogsClient/3.0 (+https://github.com/calliostro/php-discogs-api)', array $options = []): DiscogsApiClient + { + $defaultOptions = [ + 'base_uri' => 'https://api.discogs.com/', + 'timeout' => 30, + 'headers' => [ + 'User-Agent' => $userAgent, + 'Accept' => 'application/json', + 'Authorization' => sprintf('OAuth oauth_token="%s", oauth_token_secret="%s"', $token, $tokenSecret), + ], + ]; + + $guzzleClient = new Client(array_merge($defaultOptions, $options)); + + return new DiscogsApiClient($guzzleClient); + } + + /** + * Create a Discogs API client with personal access token authentication + * + * @param array $options + */ + public static function createWithToken(string $token, string $userAgent = 'DiscogsClient/3.0 (+https://github.com/calliostro/php-discogs-api)', array $options = []): DiscogsApiClient + { + $defaultOptions = [ + 'base_uri' => 'https://api.discogs.com/', + 'timeout' => 30, + 'headers' => [ + 'User-Agent' => $userAgent, + 'Accept' => 'application/json', + 'Authorization' => sprintf('Discogs token=%s', $token), + ], + ]; + + $guzzleClient = new Client(array_merge($defaultOptions, $options)); + + return new DiscogsApiClient($guzzleClient); + } +} diff --git a/src/DiscogsApiClient.php b/src/DiscogsApiClient.php new file mode 100644 index 0000000..e1c24fa --- /dev/null +++ b/src/DiscogsApiClient.php @@ -0,0 +1,165 @@ + artistGet(array $params = []) Get artist information — https://www.discogs.com/developers/#page:database,header:database-artist + * @method array artistReleases(array $params = []) Get artist releases — https://www.discogs.com/developers/#page:database,header:database-artist-releases + * @method array releaseGet(array $params = []) Get release information — https://www.discogs.com/developers/#page:database,header:database-release + * @method array releaseRatingGet(array $params = []) Get release rating — https://www.discogs.com/developers/#page:database,header:database-release-rating-by-user + * @method array releaseRatingPut(array $params = []) Set release rating — https://www.discogs.com/developers/#page:database,header:database-release-rating-by-user-post + * @method array releaseRatingDelete(array $params = []) Delete release rating — https://www.discogs.com/developers/#page:database,header:database-release-rating-by-user-delete + * @method array masterGet(array $params = []) Get master release information — https://www.discogs.com/developers/#page:database,header:database-master-release + * @method array masterVersions(array $params = []) Get master release versions — https://www.discogs.com/developers/#page:database,header:database-master-release-versions + * @method array labelGet(array $params = []) Get label information — https://www.discogs.com/developers/#page:database,header:database-label + * @method array labelReleases(array $params = []) Get label releases — https://www.discogs.com/developers/#page:database,header:database-label-releases + * @method array search(array $params = []) Search database — https://www.discogs.com/developers/#page:database,header:database-search + * + * User Identity methods: + * @method array identityGet(array $params = []) Get user identity (OAuth required) — https://www.discogs.com/developers/#page:user-identity + * @method array userGet(array $params = []) Get user profile — https://www.discogs.com/developers/#page:user-identity,header:user-identity-profile + * @method array userEdit(array $params = []) Edit user profile — https://www.discogs.com/developers/#page:user-identity,header:user-identity-profile-post + * + * Collection methods: + * @method array collectionFolders(array $params = []) Get collection folders — https://www.discogs.com/developers/#page:user-collection + * @method array collectionFolder(array $params = []) Get a collection folder — https://www.discogs.com/developers/#page:user-collection,header:user-collection-collection-folder + * @method array collectionItems(array $params = []) Get collection items by folder — https://www.discogs.com/developers/#page:user-collection,header:user-collection-collection-items-by-folder + * + * Wantlist methods: + * @method array wantlistGet(array $params = []) Get user wantlist — https://www.discogs.com/developers/#page:user-wantlist + * + * Marketplace methods: + * @method array inventoryGet(array $params = []) Get user inventory — https://www.discogs.com/developers/#page:marketplace,header:marketplace-inventory + * @method array marketplaceFee(array $params = []) Calculate marketplace fee — https://www.discogs.com/developers/#page:marketplace,header:marketplace-fee + * @method array listingGet(array $params = []) Get marketplace listing — https://www.discogs.com/developers/#page:marketplace,header:marketplace-listing + * @method array listingCreate(array $params = []) Create marketplace listing (OAuth required) — https://www.discogs.com/developers/#page:marketplace,header:marketplace-new-listing + * @method array listingUpdate(array $params = []) Update marketplace listing (OAuth required) — https://www.discogs.com/developers/#page:marketplace,header:marketplace-listing + * @method array listingDelete(array $params = []) Delete marketplace listing (OAuth required) — https://www.discogs.com/developers/#page:marketplace,header:marketplace-listing-delete + * @method array orderGet(array $params = []) Get order details (OAuth required) — https://www.discogs.com/developers/#page:marketplace,header:marketplace-order + * @method array ordersGet(array $params = []) Get orders (OAuth required) — https://www.discogs.com/developers/#page:marketplace,header:marketplace-list-orders + * @method array orderUpdate(array $params = []) Update order (OAuth required) — https://www.discogs.com/developers/#page:marketplace,header:marketplace-order-post + * @method array orderMessages(array $params = []) Get order messages (OAuth required) — https://www.discogs.com/developers/#page:marketplace,header:marketplace-list-order-messages + * @method array orderMessageAdd(array $params = []) Add an order message (OAuth required) — https://www.discogs.com/developers/#page:marketplace,header:marketplace-list-order-messages-post + */ +final class DiscogsApiClient +{ + private GuzzleClient $client; + + /** @var array */ + private array $config; + + public function __construct(GuzzleClient $client) + { + $this->client = $client; + + // Load service configuration + $this->config = require __DIR__ . '/../resources/service.php'; + } + + /** + * Magic method to call Discogs API operations + * + * Examples: + * - artistGet(['id' => '108713']) + * - search(['q' => 'Nirvana', 'type' => 'artist']) + * - releaseGet(['id' => '249504']) + * + * @param array $arguments + * @return array + */ + public function __call(string $method, array $arguments): array + { + $params = is_array($arguments[0] ?? null) ? $arguments[0] : []; + + return $this->callOperation($method, $params); + } + + /** + * @param array $params + * @return array + */ + private function callOperation(string $method, array $params): array + { + $operationName = $this->convertMethodToOperation($method); + + if (!isset($this->config['operations'][$operationName])) { + throw new \RuntimeException("Unknown operation: $operationName"); + } + + $operation = $this->config['operations'][$operationName]; + + try { + $httpMethod = $operation['httpMethod'] ?? 'GET'; + $uri = $this->buildUri($operation['uri'] ?? '', $params); + + if ($httpMethod === 'POST') { + $response = $this->client->post($uri, ['json' => $params]); + } elseif ($httpMethod === 'PUT') { + $response = $this->client->put($uri, ['json' => $params]); + } elseif ($httpMethod === 'DELETE') { + $response = $this->client->delete($uri, ['query' => $params]); + } else { + $response = $this->client->get($uri, ['query' => $params]); + } + + $body = $response->getBody()->getContents(); + $data = json_decode($body, true); + + if (json_last_error() !== JSON_ERROR_NONE) { + throw new \RuntimeException('Invalid JSON response: ' . json_last_error_msg()); + } + + if (!is_array($data)) { + throw new \RuntimeException('Expected array response from API'); + } + + if (isset($data['error'])) { + throw new \RuntimeException($data['message'] ?? 'API Error', $data['error']); + } + + return $data; + } catch (GuzzleException $e) { + throw new \RuntimeException('HTTP request failed: ' . $e->getMessage(), 0, $e); + } + } + + /** + * Convert method name to operation name + * artistGet -> artist.get + * orderMessages -> order.messages + */ + private function convertMethodToOperation(string $method): string + { + // Split a camelCase into parts + $parts = preg_split('/(?=[A-Z])/', $method, -1, PREG_SPLIT_NO_EMPTY) ?: []; + + if (!$parts) { + return $method; + } + + // Convert to dot notation + return strtolower(implode('.', $parts)); + } + + /** + * Build URI with path parameters + * + * @param array $params + */ + private function buildUri(string $uri, array $params): string + { + foreach ($params as $key => $value) { + $uri = str_replace('{' . $key . '}', (string) $value, $uri); + } + + return ltrim($uri, '/'); + } +} diff --git a/tests/Discogs/Test/ClientFactoryTest.php b/tests/Discogs/Test/ClientFactoryTest.php deleted file mode 100644 index 4057438..0000000 --- a/tests/Discogs/Test/ClientFactoryTest.php +++ /dev/null @@ -1,49 +0,0 @@ - 'php-discogs-api/2.0.0 +https://github.com/calliostro/php-discogs-api']; - $this->assertSame($default, $client->getHttpClient()->getConfig('headers')); - } - - public function testFactoryWithCustomUserAgent(): void - { - $client = ClientFactory::factory([ - 'headers' => ['User-Agent' => 'test'], - ]); - $default = ['User-Agent' => 'test']; - $this->assertSame($default, $client->getHttpClient()->getConfig('headers')); - } - - public function testFactoryWithCustomDefaultNotInClassDefaults(): void - { - $client = ClientFactory::factory([ - 'headers' => ['User-Agent' => 'test'], - 'query' => [ - 'key' => 'my-key', - 'secret' => 'my-secret', - ], - ]); - $default_headers = ['User-Agent' => 'test']; - $default_query = [ - 'key' => 'my-key', - 'secret' => 'my-secret', - ]; - $this->assertSame($default_headers, $client->getHttpClient()->getConfig('headers')); - $this->assertSame($default_query, $client->getHttpClient()->getConfig('query')); - } -} diff --git a/tests/Discogs/Test/ClientTest.php b/tests/Discogs/Test/ClientTest.php deleted file mode 100644 index 605460c..0000000 --- a/tests/Discogs/Test/ClientTest.php +++ /dev/null @@ -1,453 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Discogs\Test; - -use Discogs\ClientFactory; -use Discogs\DiscogsClient; -use GuzzleHttp\Command\Exception\CommandException; -use GuzzleHttp\Middleware; -use GuzzleHttp\Handler\MockHandler; -use GuzzleHttp\HandlerStack; -use GuzzleHttp\Psr7\Response; -use PHPUnit\Framework\TestCase; - -final class ClientTest extends TestCase -{ - public function testGetArtist(): void - { - $container = []; - $history = Middleware::History($container); - $client = $this->createClient('get_artist', $history); - $response = $client->getArtist([ - 'id' => 45 - ]); - $this->assertSame($response['id'], 45); - $this->assertSame($response['name'], 'Aphex Twin'); - $this->assertSame($response['realname'], 'Richard David James'); - $this->assertIsArray($response['images']); - $this->assertCount(9, $response['images']); - - $this->assertSame('https://api.discogs.com/artists/45', strval($container[0]['request']->getUri())); - $this->assertSame('GET', $container[0]['request']->getMethod()); - } - - public function testGetArtistReleases(): void - { - - $container = []; - $history = Middleware::History($container); - $client = $this->createClient('get_artist_releases', $history); - - $response = $client->getArtistReleases([ - 'id' => 45, - 'per_page' => 50, - 'page' => 1 - ]); - $this->assertCount(50, $response['releases']); - $this->assertArrayHasKey('pagination', $response); - $this->assertArrayHasKey('per_page', $response['pagination']); - - $this->assertSame( - 'https://api.discogs.com/artists/45/releases?per_page=50&page=1', - strval($container[0]['request']->getUri()) - ); - $this->assertSame('GET', $container[0]['request']->getMethod()); - } - - public function testSearch(): void - { - $container = []; - $history = Middleware::History($container); - $client = $this->createClient('search', $history); - - $response = $client->search([ - 'q' => 'prodigy', - 'type' => 'release', - 'title' => 'the fat of the land', - 'per_page' => 100, - 'page' => 3 - ]); - $this->assertCount(50, $response['results']); - $this->assertArrayHasKey('pagination', $response); - $this->assertArrayHasKey('per_page', $response['pagination']); - $this->assertSame( - 'https://api.discogs.com/database/search?q=prodigy&type=release&title=the%20fat%20of%20the%20land&per_page=100&page=3', - strval($container[0]['request']->getUri()) - ); - $this->assertSame('GET', $container[0]['request']->getMethod()); - } - - public function testGetRelease(): void - { - $container = []; - $history = Middleware::History($container); - $client = $this->createClient('get_release', $history); - $response = $client->getRelease([ - 'id' => 1, - 'curr_abbr' => 'USD' - ]); - - $this->assertLessThanOrEqual(8.077169493076712, $response['lowest_price']); - $this->assertSame('Accepted', $response['status']); - $this->assertArrayHasKey('videos', $response); - $this->assertCount(6, $response['videos']); - $this->assertSame( - 'https://api.discogs.com/releases/1?curr_abbr=USD', - strval($container[0]['request']->getUri()) - ); - $this->assertSame('GET', $container[0]['request']->getMethod()); - } - - public function testGetMaster(): void - { - $container = []; - $history = Middleware::History($container); - $client = $this->createClient('get_master', $history); - - $response = $client->getMaster([ - 'id' => 33687 - ]); - $this->assertSame('O Fortuna', $response['title']); - $this->assertArrayHasKey('tracklist', $response); - $this->assertCount(2, $response['tracklist']); - $this->assertSame('https://api.discogs.com/masters/33687', strval($container[0]['request']->getUri())); - $this->assertSame('GET', $container[0]['request']->getMethod()); - } - - public function testGetMasterVersions(): void - { - $container = []; - $history = Middleware::History($container); - $client = $this->createClient('get_master_versions', $history); - - $response = $client->getMasterVersions([ - 'id' => 33687, - 'per_page' => 4, - 'page' => 2 - ]); - $this->assertArrayHasKey('pagination', $response); - $this->assertArrayHasKey('versions', $response); - $this->assertCount(4, $response['versions']); - $this->assertSame( - 'https://api.discogs.com/masters/33687/versions?per_page=4&page=2', - strval($container[0]['request']->getUri()) - ); - $this->assertSame('GET', $container[0]['request']->getMethod()); - } - - public function testGetLabel(): void - { - $container = []; - $history = Middleware::History($container); - $client = $this->createClient('get_label', $history); - $response = $client->getLabel([ - 'id' => 1 - ]); - $this->assertArrayHasKey('releases_url', $response); - $this->assertSame('https://api.discogs.com/labels/1/releases', $response['releases_url']); - $this->assertArrayHasKey('sublabels', $response); - $this->assertCount(6, $response['sublabels']); - $this->assertSame('https://api.discogs.com/labels/1', strval($container[0]['request']->getUri())); - $this->assertSame('GET', $container[0]['request']->getMethod()); - } - - public function testGetLabelReleases(): void - { - $container = []; - $history = Middleware::History($container); - $client = $this->createClient('get_label_releases', $history); - $response = $client->getLabelReleases([ - 'id' => 1, - 'per_page' => 2, - 'page' => 1 - ]); - - $this->assertArrayHasKey('pagination', $response); - $this->assertArrayHasKey('releases', $response); - $this->assertCount(2, $response['releases']); - $this->assertSame( - 'https://api.discogs.com/labels/1/releases?per_page=2&page=1', - strval($container[0]['request']->getUri()) - ); - $this->assertSame('GET', $container[0]['request']->getMethod()); - } - - public function testGetOAuthIdentity(): void - { - $container = []; - $history = Middleware::History($container); - $client = $this->createClient('get_oauth_identity', $history); - $response = $client->getOAuthIdentity(); - - $this->assertSame($response['username'], 'R-Search'); - $this->assertSame($response['resource_url'], 'https://api.discogs.com/users/R-Search'); - $this->assertSame($response['consumer_name'], 'RicbraDiscogsBundle'); - $this->assertSame('GET', $container[0]['request']->getMethod()); - } - - public function testGetProfile(): void - { - $container = []; - $history = Middleware::History($container); - $client = $this->createClient('get_profile', $history); - $response = $client->getProfile([ - 'username' => 'maxperei' - ]); - - $this->assertEquals(200, $container[0]['response']->getStatusCode()); - $this->assertArrayHasKey('name', $response); - $this->assertArrayHasKey('avatar_url', $response); - $this->assertArrayHasKey('home_page', $response); - $this->assertArrayNotHasKey('email', $response); - $this->assertSame($response['name'], '∴'); - $this->assertSame( - $response['avatar_url'], - 'https://img.discogs.com/mDaw_OUjHspYLj77C_tcobr2eXc=/500x500/filters:strip_icc():format(jpeg):quality(40)/discogs-avatars/U-1861520-1498224434.jpeg.jpg' - ); - $this->assertSame($response['home_page'], 'http://maxperei.info'); - $this->assertSame('https://api.discogs.com/users/maxperei', strval($container[0]['request']->getUri())); - } - - public function testGetInventory(): void - { - $container = []; - $history = Middleware::History($container); - $client = $this->createClient('get_inventory', $history); - $response = $client->getInventory([ - 'username' => '360vinyl', - 'sort' => 'price', - 'sort_order' => 'asc' - ]); - - $this->assertArrayHasKey('pagination', $response); - $this->assertArrayHasKey('listings', $response); - $this->assertCount(50, $response['listings']); - $this->assertSame('GET', $container[0]['request']->getMethod()); - $this->assertSame( - 'https://api.discogs.com/users/360vinyl/inventory?sort=price&sort_order=asc', - strval($container[0]['request']->getUri()) - ); - } - - public function testGetOrders(): void - { - $container = []; - $history = Middleware::History($container); - $client = $this->createClient('get_orders', $history); - $response = $client->getOrders([ - 'status' => 'New Order', - 'sort' => 'price', - 'sort_order' => 'asc' - ]); - - $this->assertArrayHasKey('pagination', $response); - $this->assertArrayHasKey('orders', $response); - $this->assertCount(1, $response['orders']); - $this->assertSame('GET', $container[0]['request']->getMethod()); - $this->assertSame( - 'https://api.discogs.com/marketplace/orders?status=New%20Order&sort=price&sort_order=asc', - strval($container[0]['request']->getUri()) - ); - } - - public function testGetOrder(): void - { - $container = []; - $history = Middleware::History($container); - $client = $this->createClient('get_order', $history); - $response = $client->getOrder([ - 'order_id' => '1-1' - ]); - - $this->assertArrayHasKey('id', $response); - $this->assertArrayHasKey('resource_url', $response); - $this->assertArrayHasKey('messages_url', $response); - $this->assertArrayHasKey('uri', $response); - $this->assertArrayHasKey('status', $response); - $this->assertArrayHasKey('next_status', $response); - $this->assertArrayHasKey('fee', $response); - $this->assertArrayHasKey('created', $response); - $this->assertArrayHasKey('shipping', $response); - $this->assertArrayHasKey('shipping_address', $response); - $this->assertArrayHasKey('additional_instructions', $response); - $this->assertArrayHasKey('seller', $response); - $this->assertArrayHasKey('last_activity', $response); - $this->assertArrayHasKey('buyer', $response); - $this->assertArrayHasKey('total', $response); - $this->assertCount(1, $response['items']); - $this->assertSame('GET', $container[0]['request']->getMethod()); - $this->assertSame('https://api.discogs.com/marketplace/orders/1-1', strval($container[0]['request']->getUri())); - } - - public function testChangeOrder(): void - { - $container = []; - $history = Middleware::History($container); - $client = $this->createClient('change_order', $history); - $client->changeOrder([ - 'order_id' => '1-1', - 'shipping' => 5.0 - ]); - - $this->assertSame('POST', $container[0]['request']->getMethod()); - $this->assertSame('https://api.discogs.com/marketplace/orders/1-1', strval($container[0]['request']->getUri())); - } - - public function testCreateListingValidation(): void - { - $container = []; - $history = Middleware::History($container); - $this->expectException(CommandException::class); - $client = $this->createClient('create_listing', $history); - $client->createListing([ - 'release_id' => '1', - 'condition' => 'Mint (M)', - 'price' => 5.90 - ]); - } - - public function testCreateListing(): void - { - $container = []; - $history = Middleware::History($container); - $client = $this->createClient('create_listing', $history); - $client->createListing([ - 'release_id' => '1', - 'condition' => 'Mint (M)', - 'status' => 'For Sale', - 'price' => 5.90 - ]); - - $this->assertSame('POST', $container[0]['request']->getMethod()); - $this->assertSame('https://api.discogs.com/marketplace/listings', strval($container[0]['request']->getUri())); - } - - public function testChangeListing(): void - { - $container = []; - $history = Middleware::History($container); - $client = $this->createClient('change_listing', $history); - $client->changeListing([ - 'listing_id' => '123', - 'condition' => 'Mint (M)', - 'price' => 4.90 - ]); - - $this->assertSame('POST', $container[0]['request']->getMethod()); - $this->assertSame( - 'https://api.discogs.com/marketplace/listings/123', - strval($container[0]['request']->getUri()) - ); - } - - public function testDeleteListing(): void - { - $container = []; - $history = Middleware::History($container); - $client = $this->createClient('delete_listing', $history); - $client->deleteListing([ - 'listing_id' => '129242581' - ]); - - $this->assertSame('DELETE', $container[0]['request']->getMethod()); - $this->assertSame( - 'https://api.discogs.com/marketplace/listings/129242581', - strval($container[0]['request']->getUri()) - ); - } - - public function testGetCollectionFolders(): void - { - $container = []; - $history = Middleware::History($container); - $client = $this->createClient('get_collection_folders', $history); - $response = $client->getCollectionFolders([ - 'username' => 'example' - ]); - - $this->assertIsArray($response['folders']); - $this->assertCount(2, $response['folders']); - - $this->assertSame( - 'https://api.discogs.com/users/example/collection/folders', - strval($container[0]['request']->getUri()) - ); - $this->assertSame('GET', $container[0]['request']->getMethod()); - } - - public function testGetCollectionFolder(): void - { - $container = []; - $history = Middleware::History($container); - $client = $this->createClient('get_collection_folder', $history); - $response = $client->getCollectionFolder([ - 'username' => 'example', - 'folder_id' => 1 - ]); - - $this->assertSame($response['id'], 1); - $this->assertSame($response['count'], 20); - $this->assertSame($response['name'], 'Uncategorized'); - $this->assertSame($response['resource_url'], "https://api.discogs.com/users/example/collection/folders/1"); - - $this->assertSame( - 'https://api.discogs.com/users/example/collection/folders/1', - strval($container[0]['request']->getUri()) - ); - $this->assertSame('GET', $container[0]['request']->getMethod()); - } - - public function testGetCollectionItemsByFolder(): void - { - $container = []; - $history = Middleware::History($container); - $client = $this->createClient('get_collection_items_by_folder', $history); - $response = $client->getCollectionItemsByFolder([ - 'username' => 'rodneyfool', - 'folder_id' => 3, - 'sort' => 'artist', - 'sort_order' => 'desc', - 'per_page' => 50, - 'page' => 1 - ]); - - $this->assertCount(1, $response['releases']); - $this->assertArrayHasKey('pagination', $response); - $this->assertArrayHasKey('per_page', $response['pagination']); - - $this->assertSame( - 'https://api.discogs.com/users/rodneyfool/collection/folders/3/releases?per_page=50&page=1', - strval($container[0]['request']->getUri()) - ); - $this->assertSame('GET', $container[0]['request']->getMethod()); - } - - protected function createClient(string $mock, callable $history): DiscogsClient - { - $json = file_get_contents(__DIR__ . "/../../fixtures/$mock.json"); - $data = json_decode($json, true); - $data['body'] = json_encode($data['body']); - $mock = new MockHandler([ - new Response( - $data['status'], - $data['headers'], - $data['body'], - $data['version'], - $data['reason'] - ) - ]); - $handler = HandlerStack::create($mock); - $handler->push($history); - $client = ClientFactory::factory(['handler' => $handler]); - - return $client; - } -} diff --git a/tests/Discogs/Test/Subscriber/ThrottleSubscriberTest.php b/tests/Discogs/Test/Subscriber/ThrottleSubscriberTest.php deleted file mode 100644 index b4b55e7..0000000 --- a/tests/Discogs/Test/Subscriber/ThrottleSubscriberTest.php +++ /dev/null @@ -1,102 +0,0 @@ -assertInstanceOf(ThrottleSubscriber::class, $throttle); - } - - public function testWithThrottle(): void - { - $throttle = 2000; // milliseconds == 2 sec - $subscriber = new ThrottleSubscriber($throttle); - - $before = microtime(true); - - $handler = HandlerStack::create(new MockHandler([ - new Response(429), - new Response(200) - ])); - $handler->push(Middleware::retry($subscriber->decider(), $subscriber->delay())); - $client = new Client(['handler' => $handler]); - $client->request('GET', '/'); - - $after = microtime(true); - - $difference = $after - $before; - // Should be at least 2 seconds (with exponential backoff: 2000ms * 2^1 = 4000ms) - // Allow some tolerance for system variations (3.5 seconds minimum) - $this->assertGreaterThan(3.5, $difference, 'Throttling should delay at least 3.5 seconds'); - } - - public function testWithoutThrottle(): void - { - $throttle = 0; - $subscriber = new ThrottleSubscriber($throttle); - - $before = microtime(true); - - $handler = HandlerStack::create(new MockHandler([ - new Response(429), - new Response(200) - ])); - $handler->push(Middleware::retry($subscriber->decider(), $subscriber->delay())); - $client = new Client(['handler' => $handler]); - $client->request('GET', '/'); - - $after = microtime(true); - - $difference = $after - $before; - // Should be at max 0.5 seconds on a very slow system, tricky to test - $this->assertTrue($difference < 0.5); - } - - public function testMaxRetries(): void - { - $throttle = 500; - $max_retries = 2; - $subscriber = new ThrottleSubscriber($throttle, $max_retries); - - $before = microtime(true); - - $handler = HandlerStack::create(new MockHandler([ - new Response(429), - new Response(429), - new Response(429), - ])); - $handler->push(Middleware::retry($subscriber->decider(), $subscriber->delay())); - $client = new Client(['handler' => $handler]); - try { - $client->request('GET', '/'); - } catch (Exception $e) { - $this->assertInstanceOf(ClientException::class, $e); - $this->assertEquals(429, $e->getCode()); - } - - $after = microtime(true); - $difference = $after - $before; - - $this->assertTrue($difference > 3); - } -} diff --git a/tests/Integration/ClientWorkflowTest.php b/tests/Integration/ClientWorkflowTest.php new file mode 100644 index 0000000..5f38f91 --- /dev/null +++ b/tests/Integration/ClientWorkflowTest.php @@ -0,0 +1,155 @@ + $data + */ + private function jsonEncode(array $data): string + { + return json_encode($data) ?: '{}'; + } + + public function testCompleteWorkflowWithFactoryAndApiCalls(): void + { + // Create a mock handler with multiple responses + $mockHandler = new MockHandler([ + new Response(200, [], $this->jsonEncode(['id' => '108713', 'name' => 'Aphex Twin'])), + new Response(200, [], $this->jsonEncode(['results' => [['title' => 'Selected Ambient Works']]])), + new Response(200, [], $this->jsonEncode(['id' => '1', 'name' => 'Warp Records'])), + ]); + + $handlerStack = HandlerStack::create($mockHandler); + $guzzleClient = new Client(['handler' => $handlerStack]); + + // Create a client using factory with a custom Guzzle client + $client = new \Calliostro\Discogs\DiscogsApiClient($guzzleClient); + + // Test multiple API calls + $artist = $client->artistGet(['id' => '108713']); + $this->assertEquals('Aphex Twin', $artist['name']); + + $search = $client->search(['q' => 'Aphex Twin', 'type' => 'artist']); + $this->assertArrayHasKey('results', $search); + + $label = $client->labelGet(['id' => '1']); + $this->assertEquals('Warp Records', $label['name']); + } + + public function testFactoryCreatesWorkingClients(): void + { + // Test regular factory method + $client1 = ClientFactory::create(); + $this->assertInstanceOf(\Calliostro\Discogs\DiscogsApiClient::class, $client1); + + // Test OAuth factory method + $client2 = ClientFactory::createWithOAuth('token', 'secret'); + $this->assertInstanceOf(\Calliostro\Discogs\DiscogsApiClient::class, $client2); + + // Test token factory method + $client3 = ClientFactory::createWithToken('personal_token'); + $this->assertInstanceOf(\Calliostro\Discogs\DiscogsApiClient::class, $client3); + } + + public function testServiceConfigurationIsLoaded(): void + { + $client = ClientFactory::create(); + + // This will fail if service.php is not properly loaded. + // We use reflection to check the config was loaded + $reflection = new \ReflectionClass($client); + $configProperty = $reflection->getProperty('config'); + $configProperty->setAccessible(true); + $config = $configProperty->getValue($client); + + $this->assertIsArray($config); + $this->assertArrayHasKey('operations', $config); + $this->assertArrayHasKey('artist.get', $config['operations']); + $this->assertArrayHasKey('search', $config['operations']); + } + + public function testMethodNameToOperationConversion(): void + { + // Create a client with a mock that we'll never use + // We just want to test the method name conversion + $mockHandler = new MockHandler(); + $handlerStack = HandlerStack::create($mockHandler); + $guzzleClient = new Client(['handler' => $handlerStack]); + $client = new \Calliostro\Discogs\DiscogsApiClient($guzzleClient); + + // Use reflection to test the private method + $reflection = new \ReflectionClass($client); + $method = $reflection->getMethod('convertMethodToOperation'); + $method->setAccessible(true); + + // Test various conversions + $this->assertEquals('artist.get', $method->invokeArgs($client, ['artistGet'])); + $this->assertEquals('artist.releases', $method->invokeArgs($client, ['artistReleases'])); + $this->assertEquals('collection.folders', $method->invokeArgs($client, ['collectionFolders'])); + $this->assertEquals('order.messages', $method->invokeArgs($client, ['orderMessages'])); + $this->assertEquals('order.message.add', $method->invokeArgs($client, ['orderMessageAdd'])); + } + + public function testUriBuilding(): void + { + // Create a client to test URI building + $mockHandler = new MockHandler(); + $handlerStack = HandlerStack::create($mockHandler); + $guzzleClient = new Client(['handler' => $handlerStack]); + $client = new \Calliostro\Discogs\DiscogsApiClient($guzzleClient); + + // Use reflection to test the private method + $reflection = new \ReflectionClass($client); + $method = $reflection->getMethod('buildUri'); + $method->setAccessible(true); + + // Test URI building with parameters + $uri = $method->invokeArgs($client, ['artists/{id}', ['id' => '108713']]); + $this->assertEquals('artists/108713', $uri); + + $uri = $method->invokeArgs($client, ['users/{username}/collection/folders/{folder_id}/releases', [ + 'username' => 'testuser', + 'folder_id' => '0', + ]]); + $this->assertEquals('users/testuser/collection/folders/0/releases', $uri); + } + + public function testErrorHandlingInCompleteWorkflow(): void + { + // Create mock handler with error response + $mockHandler = new MockHandler([ + new Response(404, [], $this->jsonEncode([ + 'error' => 404, + 'message' => 'Artist not found', + ])), + ]); + + $handlerStack = HandlerStack::create($mockHandler); + $guzzleClient = new Client(['handler' => $handlerStack]); + $client = new \Calliostro\Discogs\DiscogsApiClient($guzzleClient); + + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Artist not found'); + + $client->artistGet(['id' => '999999']); + } +} diff --git a/tests/Unit/ClientFactoryTest.php b/tests/Unit/ClientFactoryTest.php new file mode 100644 index 0000000..f61f9c1 --- /dev/null +++ b/tests/Unit/ClientFactoryTest.php @@ -0,0 +1,80 @@ +assertInstanceOf(DiscogsApiClient::class, $client); + } + + public function testCreateWithOAuthReturnsDiscogsApiClient(): void + { + $client = ClientFactory::createWithOAuth('token', 'secret'); + + $this->assertInstanceOf(DiscogsApiClient::class, $client); + } + + public function testCreateWithTokenReturnsDiscogsApiClient(): void + { + $client = ClientFactory::createWithToken('personal_access_token'); + + $this->assertInstanceOf(DiscogsApiClient::class, $client); + } + + public function testCreateWithCustomUserAgentReturnsDiscogsApiClient(): void + { + $client = ClientFactory::create('CustomApp/1.0'); + + $this->assertInstanceOf(DiscogsApiClient::class, $client); + } + + public function testCreateWithAllParametersReturnsDiscogsApiClient(): void + { + $options = ['timeout' => 60]; + $client = ClientFactory::create('CustomApp/1.0', $options); + + $this->assertInstanceOf(DiscogsApiClient::class, $client); + } + + public function testCreateWithOAuthAndAllParameters(): void + { + $token = 'test_access_token'; + $tokenSecret = 'test_access_token_secret'; + $userAgent = 'CustomApp/1.0'; + $options = ['timeout' => 60]; + + $client = ClientFactory::createWithOAuth( + $token, + $tokenSecret, + $userAgent, + $options + ); + + $this->assertInstanceOf(DiscogsApiClient::class, $client); + } + + public function testCreateWithTokenAndAllParameters(): void + { + $token = 'test_personal_token'; + $userAgent = 'CustomApp/1.0'; + $options = ['timeout' => 60]; + + $client = ClientFactory::createWithToken($token, $userAgent, $options); + + $this->assertInstanceOf(DiscogsApiClient::class, $client); + } +} diff --git a/tests/Unit/DiscogsApiClientTest.php b/tests/Unit/DiscogsApiClientTest.php new file mode 100644 index 0000000..7817e97 --- /dev/null +++ b/tests/Unit/DiscogsApiClientTest.php @@ -0,0 +1,451 @@ +mockHandler = new MockHandler(); + $handlerStack = HandlerStack::create($this->mockHandler); + $guzzleClient = new Client(['handler' => $handlerStack]); + + $this->client = new DiscogsApiClient($guzzleClient); + } + + /** + * Helper method to safely encode JSON for Response body + * + * @param array $data + */ + private function jsonEncode(array $data): string + { + return json_encode($data) ?: '{}'; + } + + public function testArtistGetMethodCallsCorrectEndpoint(): void + { + $this->mockHandler->append( + new Response(200, [], $this->jsonEncode(['id' => '108713', 'name' => 'Aphex Twin'])) + ); + + $result = $this->client->artistGet(['id' => '108713']); + + $this->assertEquals(['id' => '108713', 'name' => 'Aphex Twin'], $result); + } + + public function testSearchMethodCallsCorrectEndpoint(): void + { + $this->mockHandler->append( + new Response(200, [], $this->jsonEncode(['results' => [['title' => 'Nirvana - Nevermind']]])) + ); + + $result = $this->client->search(['q' => 'Nirvana', 'type' => 'release']); + + $this->assertEquals(['results' => [['title' => 'Nirvana - Nevermind']]], $result); + } + + public function testReleaseGetMethodCallsCorrectEndpoint(): void + { + $this->mockHandler->append( + new Response(200, [], $this->jsonEncode(['id' => 249504, 'title' => 'Nevermind'])) + ); + + $result = $this->client->releaseGet(['id' => '249504']); + + $this->assertEquals(['id' => 249504, 'title' => 'Nevermind'], $result); + } + + public function testMethodNameConversionWorks(): void + { + $this->mockHandler->append( + new Response(200, [], $this->jsonEncode(['id' => '1', 'name' => 'Warp Records'])) + ); + + $result = $this->client->labelGet(['id' => '1']); + + $this->assertEquals(['id' => '1', 'name' => 'Warp Records'], $result); + } + + public function testUnknownOperationThrowsException(): void + { + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Unknown operation: unknown.method'); + + // @phpstan-ignore-next-line - Testing invalid method call + $this->client->unknownMethod(); + } + + public function testInvalidJsonResponseThrowsException(): void + { + $this->mockHandler->append( + new Response(200, [], 'invalid json') + ); + + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Invalid JSON response:'); + + $this->client->artistGet(['id' => '108713']); + } + + public function testApiErrorResponseThrowsException(): void + { + $this->mockHandler->append( + new Response(400, [], $this->jsonEncode([ + 'error' => 400, + 'message' => 'Bad Request: Invalid ID', + ])) + ); + + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Bad Request: Invalid ID'); + + $this->client->artistGet(['id' => 'invalid']); + } + + public function testApiErrorResponseWithoutMessageThrowsException(): void + { + $this->mockHandler->append( + new Response(200, [], $this->jsonEncode([ + 'error' => 400, + // No 'message' field, should use default 'API Error' + ])) + ); + + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('API Error'); + + $this->client->artistGet(['id' => '123']); + } + + public function testComplexMethodNameConversion(): void + { + $this->mockHandler->append( + new Response(200, [], $this->jsonEncode(['messages' => []])) + ); + + $result = $this->client->orderMessages(['order_id' => '123']); + + $this->assertEquals(['messages' => []], $result); + } + + public function testCollectionItemsMethod(): void + { + $this->mockHandler->append( + new Response(200, [], $this->jsonEncode(['releases' => []])) + ); + + $result = $this->client->collectionItems(['username' => 'user', 'folder_id' => '0']); + + $this->assertEquals(['releases' => []], $result); + } + + public function testPostMethodWithJsonPayload(): void + { + $this->mockHandler->append( + new Response(201, [], $this->jsonEncode(['listing_id' => '12345'])) + ); + + $result = $this->client->listingCreate([ + 'release_id' => '249504', + 'condition' => 'Mint (M)', + 'price' => '25.00', + 'status' => 'For Sale', + ]); + + $this->assertEquals(['listing_id' => '12345'], $result); + } + + public function testReleaseRatingGetMethod(): void + { + $this->mockHandler->append( + new Response(200, [], $this->jsonEncode(['username' => 'testuser', 'release_id' => 249504, 'rating' => 5])) + ); + + $result = $this->client->releaseRatingGet(['release_id' => 249504, 'username' => 'testuser']); + + $this->assertEquals(['username' => 'testuser', 'release_id' => 249504, 'rating' => 5], $result); + } + + public function testCollectionFoldersGet(): void + { + $this->mockHandler->append( + new Response(200, [], $this->jsonEncode([ + 'folders' => [ + ['id' => 0, 'name' => 'All', 'count' => 23], + ['id' => 1, 'name' => 'Uncategorized', 'count' => 20], + ], + ])) + ); + + $result = $this->client->collectionFolders(['username' => 'testuser']); + + $this->assertArrayHasKey('folders', $result); + $this->assertCount(2, $result['folders']); + } + + public function testWantlistGet(): void + { + $this->mockHandler->append( + new Response(200, [], $this->jsonEncode([ + 'wants' => [ + ['id' => 1867708, 'rating' => 4, 'basic_information' => ['title' => 'Year Zero']], + ], + ])) + ); + + $result = $this->client->wantlistGet(['username' => 'testuser']); + + $this->assertArrayHasKey('wants', $result); + $this->assertCount(1, $result['wants']); + } + + public function testMarketplaceFeeCalculation(): void + { + $this->mockHandler->append( + new Response(200, [], $this->jsonEncode(['value' => 0.42, 'currency' => 'USD'])) + ); + + $result = $this->client->marketplaceFee(['price' => 10.00]); + + $this->assertEquals(['value' => 0.42, 'currency' => 'USD'], $result); + } + + public function testListingGetMethod(): void + { + $this->mockHandler->append( + new Response(200, [], $this->jsonEncode([ + 'id' => 172723812, + 'status' => 'For Sale', + 'price' => ['currency' => 'USD', 'value' => 120], + ])) + ); + + $result = $this->client->listingGet(['listing_id' => 172723812]); + + $this->assertEquals(172723812, $result['id']); + $this->assertEquals('For Sale', $result['status']); + } + + public function testUserEdit(): void + { + $this->mockHandler->append( + new Response(200, [], $this->jsonEncode(['success' => true, 'username' => 'testuser'])) + ); + + $result = $this->client->userEdit([ + 'username' => 'testuser', + 'name' => 'Test User', + 'location' => 'Test City', + ]); + + $this->assertEquals(['success' => true, 'username' => 'testuser'], $result); + } + + public function testPutMethodHandling(): void + { + $this->mockHandler->append( + new Response(200, [], $this->jsonEncode(['rating' => 5, 'release_id' => 249504])) + ); + + $result = $this->client->releaseRatingPut([ + 'release_id' => 249504, + 'username' => 'testuser', + 'rating' => 5, + ]); + + $this->assertEquals(['rating' => 5, 'release_id' => 249504], $result); + } + + public function testDeleteMethodHandling(): void + { + $this->mockHandler->append( + new Response(204, [], '{}') + ); + + $result = $this->client->releaseRatingDelete([ + 'release_id' => 249504, + 'username' => 'testuser', + ]); + + $this->assertEquals([], $result); + } + + public function testHttpExceptionHandling(): void + { + $this->mockHandler->append( + new \GuzzleHttp\Exception\RequestException( + 'Connection failed', + new \GuzzleHttp\Psr7\Request('GET', 'test') + ) + ); + + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('HTTP request failed: Connection failed'); + + $this->client->artistGet(['id' => '123']); + } + + public function testNonArrayResponseHandling(): void + { + $this->mockHandler->append( + new Response(200, [], '"not an array"') + ); + + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Expected array response from API'); + + $this->client->artistGet(['id' => '123']); + } + + public function testUriBuilding(): void + { + $this->mockHandler->append( + new Response(200, [], $this->jsonEncode(['id' => 123, 'name' => 'Test Artist'])) + ); + + $result = $this->client->artistGet(['id' => 123]); + + $this->assertEquals(['id' => 123, 'name' => 'Test Artist'], $result); + } + + public function testComplexMethodNameConversionWithMultipleParts(): void + { + $this->mockHandler->append( + new Response(200, [], $this->jsonEncode(['messages' => []])) + ); + + $result = $this->client->orderMessageAdd([ + 'order_id' => '123-456', + 'message' => 'Test message', + ]); + + $this->assertEquals(['messages' => []], $result); + } + + public function testEmptyParametersHandling(): void + { + $this->mockHandler->append( + new Response(200, [], $this->jsonEncode(['results' => []])) + ); + + // Test calling without parameters + $result = $this->client->search(); + + $this->assertEquals(['results' => []], $result); + } + + public function testConvertMethodToOperationWithEmptyString(): void + { + $this->mockHandler->append( + new Response(200, [], $this->jsonEncode(['success' => true])) + ); + + // This will call the protected convertMethodToOperation indirectly + // by testing edge cases in method name conversion + try { + // @phpstan-ignore-next-line - Testing invalid method call + $this->client->testMethodName(); + } catch (\RuntimeException $e) { + $this->assertStringContainsString('Unknown operation', $e->getMessage()); + } + } + + public function testBuildUriWithNoParameters(): void + { + $this->mockHandler->append( + new Response(200, [], $this->jsonEncode(['results' => []])) + ); + + // Test URI building with no parameters (should not replace anything) + $result = $this->client->search(['q' => 'test']); + + $this->assertEquals(['results' => []], $result); + } + + public function testMethodCallWithNullParameters(): void + { + $this->mockHandler->append( + new Response(200, [], $this->jsonEncode(['success' => true])) + ); + + // Test method call with null as parameters - should be converted to empty array + // @phpstan-ignore-next-line - Testing parameter validation + $result = $this->client->search(null); + + $this->assertEquals(['success' => true], $result); + } + + public function testConvertMethodToOperationWithEdgeCases(): void + { + // Test the convertMethodToOperation method with edge cases + $reflection = new \ReflectionClass($this->client); + $method = $reflection->getMethod('convertMethodToOperation'); + $method->setAccessible(true); + + // Test with empty string (should return empty string) + $result = $method->invokeArgs($this->client, ['']); + $this->assertEquals('', $result); + + // Test with a single lowercase word + $result = $method->invokeArgs($this->client, ['test']); + $this->assertEquals('test', $result); + + // Test with mixed case scenarios + $result = $method->invokeArgs($this->client, ['ArtistGetReleases']); + $this->assertEquals('artist.get.releases', $result); + } + + public function testBuildUriWithComplexParameters(): void + { + // Test the buildUri method directly with complex scenarios + $reflection = new \ReflectionClass($this->client); + $method = $reflection->getMethod('buildUri'); + $method->setAccessible(true); + + // Test with leading slash + $result = $method->invokeArgs($this->client, ['/artists/{id}/releases', ['id' => '123']]); + $this->assertEquals('artists/123/releases', $result); + + // Test with no parameters to replace + $result = $method->invokeArgs($this->client, ['artists', []]); + $this->assertEquals('artists', $result); + + // Test with multiple parameters + $result = $method->invokeArgs($this->client, ['/users/{username}/collection/folders/{folder_id}', [ + 'username' => 'testuser', + 'folder_id' => '1', + 'extra' => 'ignored', // Should be ignored + ]]); + $this->assertEquals('users/testuser/collection/folders/1', $result); + } + + public function testPregSplitEdgeCaseHandling(): void + { + // Test case where preg_split might return false - this might be the missing line + $reflection = new \ReflectionClass($this->client); + $method = $reflection->getMethod('convertMethodToOperation'); + $method->setAccessible(true); + + // Test with a long method name (100 characters) to potentially trigger edge cases + $longMethodName = str_repeat('A', 100) . 'Get'; + $result = $method->invokeArgs($this->client, [$longMethodName]); + // This should still work, converting the long name properly + $this->assertIsString($result); + } +} diff --git a/tests/bootstrap.php b/tests/bootstrap.php deleted file mode 100644 index 569f3e2..0000000 --- a/tests/bootstrap.php +++ /dev/null @@ -1,11 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -require_once __DIR__.'/../vendor/autoload.php';