Files
tonehaus/docs/assignment2-project-briefing.md
T
2026-04-02 15:42:38 +01:00

970 lines
27 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Tonehaus Project Briefing
## Purpose
Tonehaus is a Symfony 7 music album review application extended for the Assignment 2 RESTful API brief. It combines a server-rendered website with a versioned JSON API, JWT authentication, external music metadata providers, and generated API documentation. The project is designed so the web UI and API share the same domain model and service layer rather than existing as separate systems.
## Core Stack
- Symfony 7
- PHP with Doctrine ORM
- Twig templates
- SQLite by default, with Postgres support
- Guzzle for external API consumption
- LexikJWTAuthenticationBundle for JWT auth
- FOSRestBundle for API response handling
- NelmioApiDocBundle for OpenAPI documentation
- PHPUnit for functional and unit tests
## Main Domain Model
- `User`: authenticated account with `ROLE_USER`, `ROLE_MODERATOR`, or `ROLE_ADMIN`
- `Album`: normalized album record, either external or user-created
- `AlbumTrack`: persisted track list for each album
- `Review`: user-authored review with title, content, rating, and timestamps
- `Setting`: key-value storage for site configuration and provider credentials
## What The Application Does
### Web Application
- lets users search for albums
- shows album pages with cover art, metadata, tracklists, review averages, and reviews
- allows logged-in users to create reviews from the album page
- allows users to create, edit, and delete their own user-created albums
- provides profile, password, and settings pages
- gives moderators/admins dashboards for moderation and site administration
- includes admin settings for external provider credentials and registration control
### REST API
The API is versioned under `/api/v1` and returns JSON.
Implemented endpoints:
- `GET /api/v1`
- `POST /api/v1/auth/login`
- `GET /api/v1/albums`
- `POST /api/v1/albums/ingest`
- `GET /api/v1/albums/{id}`
- `GET /api/v1/albums/{id}/reviews`
- `GET /api/v1/reviews`
- `GET /api/v1/reviews/{id}`
- `POST /api/v1/reviews`
- `PUT /api/v1/reviews/{id}`
- `PATCH /api/v1/reviews/{id}`
- `DELETE /api/v1/reviews/{id}`
Automatic OpenAPI documentation is available at:
- `/api/docs`
- `/api/v1/docs`
- `/api/docs.json`
- `/api/v1/docs.json`
## API Endpoint Reference
This section documents every implemented API endpoint briefly, with purpose, authentication, key inputs, and the main response shape.
Example values below are representative of the current implementation and use realistic sample IDs such as `tidal:251380836` and numeric review IDs.
### `GET /api/v1`
- purpose: API index and discovery endpoint
- auth: none
- returns: API name, version, endpoint groups, top-level `links`, and top-level `actions`
- HATEOAS: exposes `self`, `auth_login`, `albums`, `reviews`, `docs`, `docs_v1`, `schema`, and `schema_v1`
Example request:
```http
GET /api/v1
Accept: application/json
```
Example response:
```json
{
"name": "Tonehaus API",
"version": "v1",
"endpoints": {
"auth": ["POST /api/v1/auth/login"],
"albums": [
"GET /api/v1/albums",
"POST /api/v1/albums/ingest (JWT)",
"GET /api/v1/albums/{id}",
"GET /api/v1/albums/{id}/reviews"
],
"reviews": [
"GET /api/v1/reviews",
"GET /api/v1/reviews/{id}",
"POST /api/v1/reviews (JWT)",
"PUT /api/v1/reviews/{id} (JWT)",
"PATCH /api/v1/reviews/{id} (JWT)",
"DELETE /api/v1/reviews/{id} (JWT)"
]
},
"links": {
"self": "/api/v1",
"auth_login": "/api/v1/auth/login",
"albums": "/api/v1/albums",
"reviews": "/api/v1/reviews",
"docs": "/api/docs"
},
"actions": {
"login": {
"method": "POST",
"href": "/api/v1/auth/login"
}
}
}
```
### `POST /api/v1/auth/login`
- purpose: authenticate a user and issue a JWT for protected routes
- auth: none
- request body: `email`, `password`
- success response: `200` with `{ "token": "..." }`
- error response: `401` for invalid credentials
Example request:
```http
POST /api/v1/auth/login
Content-Type: application/json
Accept: application/json
{
"email": "user@example.com",
"password": "password123"
}
```
Example success response:
```json
{
"token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9..."
}
```
Example error response:
```json
{
"error": {
"code": "invalid_credentials",
"message": "Invalid credentials.",
"status": 401
}
}
```
### `GET /api/v1/albums`
- purpose: search albums from the local catalog and the currently active external provider
- auth: none
- query parameters: `query`, `album`, `artist`, `genre`, `year_from`, `year_to`, `source`, `limit`, `mode`
- returns:
- `meta` with source, limit, query, mode, and search profile
- `data` as album resources
- `stats` for review aggregates
- `saved_ids` for already-ingested external albums
- collection-level `links` and `actions`
- HATEOAS:
- collection links include `self`, `root`, and `ingest`
- action metadata includes `ingest_album`
- each album resource is decorated with its own `links` and `actions`
Example request:
```http
GET /api/v1/albums?query=4%3A44&artist=JAY-Z&limit=5&mode=fast
Accept: application/json
```
Example response:
```json
{
"meta": {
"source": "all",
"limit": 5,
"query": "4:44",
"mode": "fast"
},
"data": [
{
"id": "tidal:251380836",
"name": "4:44",
"images": [
{ "url": "https://cdn.example.test/320.jpg" }
],
"artists": [
{ "name": "JAY-Z" }
],
"genres": ["Hip Hop"],
"release_date": "2017-06-30",
"total_tracks": 10,
"external_urls": {
"external": "https://tidal.com/browse/album/251380836"
},
"source": "external",
"links": {
"self": "/api/v1/albums/tidal%3A251380836",
"reviews": "/api/v1/albums/tidal%3A251380836/reviews",
"open_source": "https://tidal.com/browse/album/251380836"
},
"actions": {
"create_review": {
"method": "POST",
"href": "/api/v1/reviews",
"requires_auth": true,
"payload": {
"album_id": "tidal:251380836"
}
},
"ingest": {
"method": "POST",
"href": "/api/v1/albums/ingest",
"requires_auth": true,
"payload": {
"external_id": "tidal:251380836"
}
}
}
}
],
"stats": {
"tidal:251380836": {
"count": 2,
"avg": 8.5
}
},
"saved_ids": ["tidal:251380836"],
"links": {
"self": "/api/v1/albums?source=all&limit=5&mode=fast&query=4%3A44&artist=JAY-Z",
"root": "/api/v1",
"ingest": "/api/v1/albums/ingest"
},
"actions": {
"ingest_album": {
"method": "POST",
"href": "/api/v1/albums/ingest",
"requires_auth": true
}
}
}
```
### `POST /api/v1/albums/ingest`
- purpose: persist an external album into the local database
- auth: JWT required
- request body:
- either `external_id`
- or `query`
- or `artist` and `album`
- success response: `201` with `data` for the ingested album and `tracks`
- main error responses:
- `400` invalid JSON or missing lookup input
- `401` missing/invalid JWT
- `404` external album not found
- `422` album missing required metadata or tracks
Example request:
```http
POST /api/v1/albums/ingest
Authorization: Bearer <jwt-token>
Content-Type: application/json
Accept: application/json
{
"external_id": "tidal:251380836"
}
```
Example response:
```json
{
"data": {
"id": "tidal:251380836",
"name": "4:44",
"artists": [
{ "name": "JAY-Z" }
],
"genres": ["Hip Hop"],
"release_date": "2017-06-30",
"total_tracks": 10,
"source": "external",
"links": {
"self": "/api/v1/albums/tidal%3A251380836",
"reviews": "/api/v1/albums/tidal%3A251380836/reviews"
}
},
"tracks": [
{
"id": "track-1",
"disc": 1,
"track": 1,
"name": "Kill Jay Z",
"duration_ms": 205000
}
]
}
```
### `GET /api/v1/albums/{id}`
- purpose: fetch one album by public identifier
- auth: none
- path parameter: `id` string public album ID such as a provider ID or user album ID
- returns:
- `data` for one album
- `tracks` list
- `stats` with review count and average rating
- behaviour:
- if the album is not yet local but is available from the external provider, it can be hydrated and persisted automatically
- HATEOAS on `data`:
- links include `self`, `reviews`, and `open_source` when available
- actions include `create_review`
- external albums also expose `ingest`
Example request:
```http
GET /api/v1/albums/tidal%3A251380836
Accept: application/json
```
Example response:
```json
{
"data": {
"id": "tidal:251380836",
"name": "4:44",
"images": [
{ "url": "https://cdn.example.test/640.jpg" }
],
"artists": [
{ "name": "JAY-Z" }
],
"genres": ["Hip Hop"],
"release_date": "2017-06-30",
"total_tracks": 10,
"external_urls": {
"external": "https://tidal.com/browse/album/251380836"
},
"source": "external",
"links": {
"self": "/api/v1/albums/tidal%3A251380836",
"reviews": "/api/v1/albums/tidal%3A251380836/reviews",
"open_source": "https://tidal.com/browse/album/251380836"
},
"actions": {
"create_review": {
"method": "POST",
"href": "/api/v1/reviews",
"requires_auth": true,
"payload": {
"album_id": "tidal:251380836"
}
},
"ingest": {
"method": "POST",
"href": "/api/v1/albums/ingest",
"requires_auth": true,
"payload": {
"external_id": "tidal:251380836"
}
}
}
},
"tracks": [
{
"id": "track-1",
"disc": 1,
"track": 1,
"name": "Kill Jay Z",
"duration_ms": 205000
}
],
"stats": {
"count": 2,
"avg": 8.5
}
}
```
### `GET /api/v1/albums/{id}/reviews`
- purpose: list reviews for a specific album
- auth: none
- path parameter: `id` string public album ID
- returns:
- `data` as review resources scoped to that album
- collection `links`
- collection `actions`
- HATEOAS:
- collection links include `self`, `album`, and `reviews`
- collection actions include `create_review`
- each review item includes links back to its own item route and related album routes
Example request:
```http
GET /api/v1/albums/tidal%3A251380836/reviews
Accept: application/json
```
Example response:
```json
{
"data": [
{
"id": 12,
"title": "A dense and rewarding record",
"content": "This album becomes stronger on every listen and the production is excellent.",
"rating": 9,
"author": {
"id": 4,
"display_name": "Jake Andrews"
},
"created_at": "2026-04-02T11:40:21+00:00",
"updated_at": "2026-04-02T11:40:21+00:00",
"links": {
"self": "/api/v1/reviews/12",
"album": "/api/v1/albums/tidal%3A251380836",
"album_reviews": "/api/v1/albums/tidal%3A251380836/reviews"
}
}
],
"links": {
"self": "/api/v1/albums/tidal%3A251380836/reviews",
"album": "/api/v1/albums/tidal%3A251380836",
"reviews": "/api/v1/reviews"
},
"actions": {
"create_review": {
"method": "POST",
"href": "/api/v1/reviews",
"requires_auth": true
}
}
}
```
### `GET /api/v1/reviews`
- purpose: list the latest reviews
- auth: none
- query parameters: `limit` from `1` to `100`, default `50`
- returns:
- `data` as review resources
- collection `links`
- collection `actions`
- HATEOAS:
- collection links include `self`, `root`, and `create`
- collection actions include `create_review`
Example request:
```http
GET /api/v1/reviews?limit=10
Accept: application/json
```
Example response:
```json
{
"data": [
{
"id": 12,
"title": "A dense and rewarding record",
"content": "This album becomes stronger on every listen and the production is excellent.",
"rating": 9,
"created_at": "2026-04-02T11:40:21+00:00",
"updated_at": "2026-04-02T11:40:21+00:00",
"author": {
"id": 4,
"display_name": "Jake Andrews"
},
"album": {
"id": "tidal:251380836",
"name": "4:44",
"source": "external"
},
"links": {
"self": "/api/v1/reviews/12",
"album": "/api/v1/albums/tidal%3A251380836",
"album_reviews": "/api/v1/albums/tidal%3A251380836/reviews"
}
}
],
"links": {
"self": "/api/v1/reviews?limit=10",
"root": "/api/v1",
"create": "/api/v1/reviews"
},
"actions": {
"create_review": {
"method": "POST",
"href": "/api/v1/reviews",
"requires_auth": true
}
}
}
```
### `GET /api/v1/reviews/{id}`
- purpose: fetch one review by numeric ID
- auth: none
- path parameter: `id` integer review ID
- returns: one review resource in `data`
- HATEOAS on the review resource:
- links include `self`, `album`, and `album_reviews`
- if the current user is allowed to change the review, action metadata can include `update` and `delete`
Example request:
```http
GET /api/v1/reviews/12
Accept: application/json
```
Example response:
```json
{
"data": {
"id": 12,
"title": "A dense and rewarding record",
"content": "This album becomes stronger on every listen and the production is excellent.",
"rating": 9,
"created_at": "2026-04-02T11:40:21+00:00",
"updated_at": "2026-04-02T11:40:21+00:00",
"author": {
"id": 4,
"display_name": "Jake Andrews"
},
"album": {
"id": "tidal:251380836",
"name": "4:44",
"source": "external"
},
"links": {
"self": "/api/v1/reviews/12",
"album": "/api/v1/albums/tidal%3A251380836",
"album_reviews": "/api/v1/albums/tidal%3A251380836/reviews"
}
}
}
```
### `POST /api/v1/reviews`
- purpose: create a new review for an existing album
- auth: JWT required
- request body:
- `album_id`
- `title`
- `content`
- `rating`
- validation rules:
- `title` required
- `content` required and minimum 20 characters
- `rating` must be between 1 and 10
- `album_id` must resolve to a valid album with tracks
- success response: `201` with the created review resource
- main error responses:
- `400` invalid JSON
- `401` authentication required
- `404` album not found
- `422` validation failure
Example request:
```http
POST /api/v1/reviews
Authorization: Bearer <jwt-token>
Content-Type: application/json
Accept: application/json
{
"album_id": "tidal:251380836",
"title": "A dense and rewarding record",
"content": "This album becomes stronger on every listen and the production is excellent.",
"rating": 9
}
```
Example response:
```json
{
"data": {
"id": 12,
"title": "A dense and rewarding record",
"content": "This album becomes stronger on every listen and the production is excellent.",
"rating": 9,
"created_at": "2026-04-02T11:40:21+00:00",
"updated_at": "2026-04-02T11:40:21+00:00",
"author": {
"id": 4,
"display_name": "Jake Andrews"
},
"album": {
"id": "tidal:251380836",
"name": "4:44",
"source": "external"
},
"links": {
"self": "/api/v1/reviews/12",
"album": "/api/v1/albums/tidal%3A251380836",
"album_reviews": "/api/v1/albums/tidal%3A251380836/reviews"
}
}
}
```
### `PUT /api/v1/reviews/{id}`
- purpose: update an existing review
- auth: JWT required
- path parameter: `id` integer review ID
- accepted body fields: `title`, `content`, `rating`
- authorization: review author, moderator, or admin
- success response: `200` with updated review resource
- main error responses:
- `400` invalid JSON
- `401` missing/invalid JWT
- `403` forbidden
- `422` validation failure
- implementation note: `PUT` currently shares the same update handler as `PATCH`
Example request:
```http
PUT /api/v1/reviews/12
Authorization: Bearer <jwt-token>
Content-Type: application/json
Accept: application/json
{
"title": "Revisited after a week",
"content": "The writing is sharper than I first thought and the production has a lot more depth on repeat listens.",
"rating": 8
}
```
Example response:
```json
{
"data": {
"id": 12,
"title": "Revisited after a week",
"content": "The writing is sharper than I first thought and the production has a lot more depth on repeat listens.",
"rating": 8,
"created_at": "2026-04-02T11:40:21+00:00",
"updated_at": "2026-04-02T12:05:44+00:00",
"author": {
"id": 4,
"display_name": "Jake Andrews"
},
"album": {
"id": "tidal:251380836",
"name": "4:44",
"source": "external"
},
"links": {
"self": "/api/v1/reviews/12",
"album": "/api/v1/albums/tidal%3A251380836",
"album_reviews": "/api/v1/albums/tidal%3A251380836/reviews"
}
}
}
```
### `PATCH /api/v1/reviews/{id}`
- purpose: partially update an existing review
- auth: JWT required
- path parameter: `id` integer review ID
- accepted body fields: `title`, `content`, `rating`
- authorization: review author, moderator, or admin
- success response: `200` with updated review resource
- main error responses:
- `400` invalid JSON
- `401` missing/invalid JWT
- `403` forbidden
- `422` validation failure
Example request:
```http
PATCH /api/v1/reviews/12
Authorization: Bearer <jwt-token>
Content-Type: application/json
Accept: application/json
{
"rating": 10
}
```
Example response:
```json
{
"data": {
"id": 12,
"title": "A dense and rewarding record",
"content": "This album becomes stronger on every listen and the production is excellent.",
"rating": 10,
"created_at": "2026-04-02T11:40:21+00:00",
"updated_at": "2026-04-02T12:10:03+00:00",
"author": {
"id": 4,
"display_name": "Jake Andrews"
},
"album": {
"id": "tidal:251380836",
"name": "4:44",
"source": "external"
},
"links": {
"self": "/api/v1/reviews/12",
"album": "/api/v1/albums/tidal%3A251380836",
"album_reviews": "/api/v1/albums/tidal%3A251380836/reviews"
}
}
}
```
### `DELETE /api/v1/reviews/{id}`
- purpose: delete a review
- auth: JWT required
- path parameter: `id` integer review ID
- authorization: review author, moderator, or admin
- success response: `204 No Content`
- main error responses:
- `401` missing/invalid JWT
- `403` forbidden
- `404` not found
Example request:
```http
DELETE /api/v1/reviews/12
Authorization: Bearer <jwt-token>
Accept: application/json
```
Example response:
```http
204 No Content
```
## Architecture Summary
The project follows standard Symfony separation of concerns.
- controllers handle HTTP orchestration only
- services contain business logic and external API integration
- repositories handle domain-specific queries
- entities define persisted state
- forms handle server-side validation for web submissions
- security config and voters enforce route access and ownership rules
- event subscribers normalize API errors into JSON responses
This matters for the brief because the API is not implemented as one large controller. It is structured in a maintainable way using framework conventions and reusable services.
## API Implementation Summary
The API implementation is centered around resource-based routes rather than action-style routes. Review endpoints expose collection and item resources, while album endpoints expose search, detail, ingestion, and album-specific review resources.
Key implementation points:
- uses descriptive REST-style URIs such as `/api/v1/reviews` and `/api/v1/albums/{id}/reviews`
- uses correct HTTP verbs for retrieval, creation, update, and deletion
- returns JSON for both success and failure cases
- includes HATEOAS-style hypermedia in API responses through `links` and `actions`
- uses JWT authentication for protected write operations
- excludes sensitive data such as user email and password hashes from API responses
- uses structured status codes including `200`, `201`, `204`, `401`, `403`, `404`, and `422`
- provides generated OpenAPI documentation through Nelmio
The review API supports full CRUD behaviour for third-party clients. Public reads are available without authentication, while create, update, and delete require authenticated users. Authorization is more specific than simple login checks because review ownership is enforced with a `ReviewVoter`, allowing moderators and admins to manage any review while normal users can only manage their own.
The API also includes a useful sub-resource:
- `GET /api/v1/albums/{id}/reviews`
This improves the resource model because reviews can be queried globally or in the context of a specific album.
The API also now includes a lightweight HATEOAS layer. The base API index exposes navigational links such as `self`, `albums`, `reviews`, and `docs`, plus action metadata for login. Album and review resources expose their own `links` and, where relevant, `actions`, for example:
- album resources link to `self`, `reviews`, and `open_source`
- album resources expose actions such as `create_review` and `ingest`
- review resources link back to their own item route and related album routes
- review collections expose a `create_review` action for discoverability
This improves the API from a client perspective because consumers do not need to hard-code every related route manually. Instead, the response itself advertises relevant next operations.
## API Consumption Summary
The application consumes external music APIs through Guzzle and a provider abstraction rather than binding the UI directly to one upstream service.
Implemented external providers:
- Last.fm
- MusicBrainz
- TIDAL
Provider logic is normalized behind `ExternalAlbumClientInterface`, with concrete clients for each provider. This means the rest of the application receives one consistent album payload structure regardless of the source.
The consumed API data is used to:
- search external album catalogs
- populate album metadata
- fetch artists, genres, release dates, and track lists
- fetch cover art
- ingest external albums into the local database
- enrich album detail pages and search results in the UI
This is deep integration rather than a token demo call. External data directly affects the search experience, album display, save/ingest flow, and review context.
## External Integration Details
### Last.fm
Used for album search and metadata enrichment. Responses are cached and normalized into the project’s album format.
### MusicBrainz
Used as an alternative provider and for structured release metadata. The project supports optional authenticated requests and cover-art URL generation via normalized output.
### TIDAL
Used as another full external provider. The `TidalClient` implements OAuth 2.0 client credentials, caches access tokens, retries on expired auth, and converts JSON:API responses into the same normalized album contract used elsewhere.
This strengthens the project against the brief because it goes beyond a single trivial API call and includes a second non-trivial external integration path.
## Security Summary
Security is handled at multiple layers:
- JWT authentication for protected API routes
- Symfony access control rules for route-level protection
- role hierarchy: admin > moderator > user
- `ReviewVoter` for ownership-based edit/delete permissions
- normalized JSON authentication failures for API clients
- exclusion of sensitive user fields from serialized API responses
This is stronger than basic coursework authentication because it combines framework security with resource-level authorization.
## Error Handling and Robustness
API error handling is centralized so clients receive JSON errors rather than HTML exception pages.
The project includes:
- a shared API response helper
- exception subscribers that normalize failures
- JWT failure subscribers for missing, invalid, or expired tokens
- validation logic for malformed JSON and invalid field values
- server-side handling for missing albums, forbidden edits, and unsupported methods
This improves client usability and supports a more production-like API design.
## Testing Summary
The project includes both functional and unit tests.
Functional API tests cover:
- missing JWT
- invalid JWT
- malformed JSON
- validation failures
- forbidden review updates
- missing album resources
- protection of sensitive author data
- hypermedia links and action metadata on API index, album, and album-review responses
Unit tests cover provider-specific service behaviour, including TIDAL payload normalization and provider routing.
This supports the brief’s emphasis on working, demonstrable features rather than partially implemented code.
## How The Project Meets The Brief
### API Implementation
The brief requires a usable RESTful API capable of listing, reading, creating, editing, and deleting album reviews in JSON. This project meets that with:
- `GET`, `POST`, `PUT`, `PATCH`, and `DELETE` review endpoints
- JWT-protected write routes
- appropriate resource names
- JSON request and response formats
- HATEOAS-style `links` and `actions` to improve discoverability
- status-code-based error handling
- sub-resource support through album review listing
- automatically generated OpenAPI documentation
### API Consumption
The brief requires at least two significantly different examples of API interaction and integration into the review website. This project meets that with:
- external album search
- album metadata retrieval
- track list retrieval
- cover art integration
- album ingestion into local storage
- multiple provider implementations
The consumed data is visible in the website itself, especially on search results and album detail pages.
### Documentation and Report Potential
The project already has strong source material for the report because it includes:
- generated OpenAPI docs
- explicit controller routes and OpenAPI attributes
- centralized security configuration
- reusable external service classes
- tests that demonstrate expected API behaviour
## Honest Technical Caveats
- `PUT` and `PATCH` currently share the same update action, so `PUT` behaves more like a flexible update than a strict full replacement
- local verification in this shell was limited because `php` is not installed here, so PHPUnit was not run live during this session
- some observability headers in album search are still named with `Lastfm` even though the provider layer now supports multiple providers
These are not major blockers, but they are worth understanding if you want the final report to sound precise and credible.
## Strongest Points To Emphasize In A Final Report
- the API is versioned and resource-oriented
- the API now includes lightweight HATEOAS support through response `links` and `actions`
- write operations are protected with JWT rather than session auth
- review ownership is enforced through a voter, not just a login check
- external APIs are abstracted behind a provider interface
- external data is deeply integrated into the UI and persisted catalog
- OpenAPI documentation is generated automatically
- the project includes automated tests for important API behaviour
## Short Project Summary
Tonehaus is a Symfony-based music review platform extended into a RESTful web application. It implements a versioned JSON API for review CRUD and album access, secures write routes with JWT, consumes multiple external music providers through Guzzle and a normalized service layer, and integrates that data directly into album discovery, metadata display, tracklists, and review workflows. The architecture is modular, test-backed, and aligned with the main goals of the Assignment 2 brief.