diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..0db59c9 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,31 @@ +# AGENTS.md + +## Stack +- Single Rails API app. +- Use Ruby `3.1.2` (`.ruby-version`) and Bundler `2.5.22` (`Gemfile.lock`). macOS system Ruby/Bundler fails here before the app boots; switch to the project Ruby first. +- Prefer project binstubs (`bin/rails`, `bin/rake`) once the correct Ruby is active. +- There is no repo-local lint/typecheck toolchain to run; verification is Rails commands plus RSpec. + +## Setup +- `bin/setup` is the canonical bootstrap, but it assumes `config/database.yml` already exists. That file is not checked in. +- PostgreSQL is the only DB (`pg` gem; `db/schema.rb` enables `plpgsql`). After schema changes, use `bin/rails db:prepare`. +- Focused test run: `bundle exec rspec spec/path/to/file_spec.rb`. + +## Data Loading +- `bundle exec rake hp_data:build` imports `public/data/data.xlsx` into `Genre`, `School`, `SchoolHouse`, `Person`, and `Creature`. +- That importer skips each table if it already has rows, so rerunning it does not refresh existing data. +- `bin/rails db:seed` separately loads `db/seeds/*.rb` in lexical order (`001_...`, `002_...`). Keep filenames ordered if you add new seed files. + +## Code Map +- Trust `config/routes.rb` over the README: the README endpoint list is stale. +- API entrypoints live in `app/controllers/api/v1`. +- All JSON rendering goes through `app/controllers/concerns/response.rb`; shared API error handling lives in `app/controllers/concerns/exceptions.rb`. +- Response payloads are defined in `app/serializers/*` with `JSONAPI::Serializer`; controller/model changes often require serializer updates too. +- Scalar docs live at `public/docs/index.html` and load `public/openapi.yaml`; keep that spec in sync with routes, serializers, and shared error responses. +- Current domain models are `School`, `SchoolHouse`, `Genre`, `Person`, and `Creature`. There is no current `Wizard` or `Student` model in `app/models`. + +## Gotchas +- "Students" are derived from `Person.students`, which filters the misspelled `ocupation` column. Do not rename or "fix" that spelling without a real migration and data update. +- `config/routes.rb` already marks the nested `people/students` route as broken. If you touch student endpoints, update routes, docs, and specs together. +- Many request/model specs are stale scaffold leftovers or still reference removed `Wizard`/`Student` resources. Validate behavior against routes/models before trusting those specs. +- The only GitHub Actions workflow is a tag-triggered release. It is not a reliable safety net right now: the test step is commented out, and the Postgres service password does not match the workflow `DATABASE_URL` password. diff --git a/README.md b/README.md index 1de83d2..2d416ff 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ **A RESTful OPEN API for the Wizarding World.** Access data about characters, creatures, schools, and houses through a simple JSON interface. -[View Documentation](#-documentation) • [Report Bug](https://github.com/CarlosLeonCode/harry_potter_open_api/issues) • [Request Feature](https://github.com/CarlosLeonCode/harry_potter_open_api/issues) +[View Documentation](https://harry-potter-open-api-ff4c7a51ed23.herokuapp.com/docs) • [Report Bug](https://github.com/CarlosLeonCode/harry_potter_open_api/issues) • [Request Feature](https://github.com/CarlosLeonCode/harry_potter_open_api/issues) @@ -32,18 +32,12 @@ Access data about characters, creatures, schools, and houses through a simple JS https://harry-potter-open-api-ff4c7a51ed23.herokuapp.com ``` -### Endpoints +### Interactive Reference -| Resource | Method | Endpoint | Description | -| :--- | :---: | :--- | :--- | -| **Schools** | `GET` | `/api/v1/schools` | List all wizarding schools | -| **Houses** | `GET` | `/api/v1/school_houses` | List all school houses | -| **House Details** | `GET` | `/api/v1/school_houses/:id` | Get a specific house by ID | -| **Characters** | `GET` | `/api/v1/people` | List all characters | -| **Character Details** | `GET` | `/api/v1/people/:id` | Get a specific character by ID | -| **Students** | `GET` | `/api/v1/people/students` | List only students | -| **Creatures** | `GET` | `/api/v1/creatures` | List all magical creatures | -| **Creature Details** | `GET` | `/api/v1/creatures/:id` | Get a specific creature by ID | +- Browse the API in Scalar at [`/docs`](https://harry-potter-open-api-ff4c7a51ed23.herokuapp.com/docs). +- The OpenAPI source served by the app is available at [`/openapi.yaml`](https://harry-potter-open-api-ff4c7a51ed23.herokuapp.com/openapi.yaml). +- The checked-in OpenAPI file is now the documentation source of truth, alongside `config/routes.rb`. +- Student routes are intentionally excluded from the current docs because the nested route is marked broken in `config/routes.rb` and there is no matching `StudentsController` implementation in `app/controllers/api/v1`.
@@ -106,6 +100,8 @@ rails s ``` Access the API at `http://localhost:3000/api/v1/...` +Docs are available locally at `http://localhost:3000/docs`. +
diff --git a/config/routes.rb b/config/routes.rb index dc5da0c..5f09893 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,5 +1,7 @@ Rails.application.routes.draw do + get '/docs', to: redirect('/docs/index.html') + root to: redirect('https://harrypotter-open-api.netlify.app/') # -- Start api v1 routes diff --git a/public/docs/index.html b/public/docs/index.html new file mode 100644 index 0000000..20f18d2 --- /dev/null +++ b/public/docs/index.html @@ -0,0 +1,30 @@ + + + + + + Harry Potter Open API Docs + + + +
+ + + + + diff --git a/public/openapi.yaml b/public/openapi.yaml new file mode 100644 index 0000000..6bead96 --- /dev/null +++ b/public/openapi.yaml @@ -0,0 +1,581 @@ +openapi: 3.1.0 +info: + title: Harry Potter Open API + version: 1.0.0 + description: | + Read-only API reference for the Wizarding World data exposed by this Rails app. + + The interactive Scalar UI is served at `/docs` and this OpenAPI document is served at + `/openapi.yaml`. + + Student routes are intentionally omitted from this first spec pass because the current + routing/controller setup marks the nested variant as broken and does not expose a matching + `StudentsController` implementation under `app/controllers/api/v1`. +servers: + - url: / + description: Same-origin server +tags: + - name: Health + - name: Genres + - name: Schools + - name: School Houses + - name: People + - name: Creatures +paths: + /api/v1/health: + get: + tags: + - Health + summary: Health status + operationId: getHealth + responses: + '200': + description: API is reachable + content: + application/json: + schema: + $ref: '#/components/schemas/HealthResponse' + example: + status: ok + + /api/v1/genres: + get: + tags: + - Genres + summary: List genres + operationId: listGenres + responses: + '200': + description: Genres collection + content: + application/json: + schema: + $ref: '#/components/schemas/GenreCollectionResponse' + + /api/v1/genres/{id}: + get: + tags: + - Genres + summary: Get a genre + operationId: getGenre + parameters: + - $ref: '#/components/parameters/Id' + responses: + '200': + description: Genre resource + content: + application/json: + schema: + $ref: '#/components/schemas/GenreResponse' + '404': + $ref: '#/components/responses/NotFound' + + /api/v1/schools: + get: + tags: + - Schools + summary: List schools + operationId: listSchools + responses: + '200': + description: Schools collection + content: + application/json: + schema: + $ref: '#/components/schemas/SchoolCollectionResponse' + + /api/v1/schools/{id}: + get: + tags: + - Schools + summary: Get a school + operationId: getSchool + parameters: + - $ref: '#/components/parameters/Id' + responses: + '200': + description: School resource + content: + application/json: + schema: + $ref: '#/components/schemas/SchoolResponse' + '404': + $ref: '#/components/responses/NotFound' + + /api/v1/schools/{id}/houses: + get: + tags: + - Schools + - School Houses + summary: List houses for a school + operationId: listSchoolHousesForSchool + parameters: + - $ref: '#/components/parameters/Id' + responses: + '200': + description: School houses collection + content: + application/json: + schema: + $ref: '#/components/schemas/SchoolHouseCollectionResponse' + '404': + $ref: '#/components/responses/NotFound' + + /api/v1/school_houses: + get: + tags: + - School Houses + summary: List school houses + operationId: listSchoolHouses + responses: + '200': + description: School houses collection + content: + application/json: + schema: + $ref: '#/components/schemas/SchoolHouseCollectionResponse' + + /api/v1/school_houses/{id}: + get: + tags: + - School Houses + summary: Get a school house + operationId: getSchoolHouse + parameters: + - $ref: '#/components/parameters/Id' + responses: + '200': + description: School house resource + content: + application/json: + schema: + $ref: '#/components/schemas/SchoolHouseResponse' + '404': + $ref: '#/components/responses/NotFound' + + /api/v1/people: + get: + tags: + - People + summary: List people + operationId: listPeople + responses: + '200': + description: People collection + content: + application/json: + schema: + $ref: '#/components/schemas/PersonCollectionResponse' + + /api/v1/people/{id}: + get: + tags: + - People + summary: Get a person + operationId: getPerson + parameters: + - $ref: '#/components/parameters/Id' + responses: + '200': + description: Person resource + content: + application/json: + schema: + $ref: '#/components/schemas/PersonResponse' + '404': + $ref: '#/components/responses/NotFound' + + /api/v1/creatures: + get: + tags: + - Creatures + summary: List creatures + operationId: listCreatures + responses: + '200': + description: Creatures collection + content: + application/json: + schema: + $ref: '#/components/schemas/CreatureCollectionResponse' + + /api/v1/creatures/{id}: + get: + tags: + - Creatures + summary: Get a creature + operationId: getCreature + parameters: + - $ref: '#/components/parameters/Id' + responses: + '200': + description: Creature resource + content: + application/json: + schema: + $ref: '#/components/schemas/CreatureResponse' + '404': + $ref: '#/components/responses/NotFound' + +components: + parameters: + Id: + name: id + in: path + required: true + description: Record identifier + schema: + type: integer + + responses: + NotFound: + description: Resource was not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + example: + status: not_found + message: Record not found 😵‍💫 + + schemas: + HealthResponse: + type: object + additionalProperties: false + properties: + status: + type: string + example: ok + required: + - status + + ErrorResponse: + type: object + additionalProperties: false + properties: + status: + type: string + example: not_found + message: + type: string + example: Record not found 😵‍💫 + required: + - status + - message + + GenreAttributes: + type: object + additionalProperties: false + properties: + name: + type: string + example: Male + required: + - name + + GenreResource: + type: object + additionalProperties: false + properties: + id: + type: string + example: '1' + type: + type: string + example: genre + attributes: + $ref: '#/components/schemas/GenreAttributes' + required: + - id + - type + - attributes + + GenreResponse: + type: object + additionalProperties: false + properties: + data: + $ref: '#/components/schemas/GenreResource' + required: + - data + + GenreCollectionResponse: + type: object + additionalProperties: false + properties: + data: + type: array + items: + $ref: '#/components/schemas/GenreResource' + required: + - data + + SchoolAttributes: + type: object + additionalProperties: false + properties: + name: + type: string + example: Hogwarts School of Witchcraft and Wizardry + url_logo: + type: string + example: https://i.ibb.co/60HvbSm/hogwarts.jpg + required: + - name + - url_logo + + SchoolResource: + type: object + additionalProperties: false + properties: + id: + type: string + example: '1' + type: + type: string + example: school + attributes: + $ref: '#/components/schemas/SchoolAttributes' + required: + - id + - type + - attributes + + SchoolResponse: + type: object + additionalProperties: false + properties: + data: + $ref: '#/components/schemas/SchoolResource' + required: + - data + + SchoolCollectionResponse: + type: object + additionalProperties: false + properties: + data: + type: array + items: + $ref: '#/components/schemas/SchoolResource' + required: + - data + + SchoolHouseAttributes: + type: object + additionalProperties: false + properties: + name: + type: string + example: Gryffindor + url_logo: + type: string + example: https://i.ibb.co/8MJY087/Gryffindor.jpg + required: + - name + - url_logo + + SchoolHouseResource: + type: object + additionalProperties: false + properties: + id: + type: string + example: '1' + type: + type: string + example: school_house + attributes: + $ref: '#/components/schemas/SchoolHouseAttributes' + required: + - id + - type + - attributes + + SchoolHouseResponse: + type: object + additionalProperties: false + properties: + data: + $ref: '#/components/schemas/SchoolHouseResource' + required: + - data + + SchoolHouseCollectionResponse: + type: object + additionalProperties: false + properties: + data: + type: array + items: + $ref: '#/components/schemas/SchoolHouseResource' + required: + - data + + PersonAttributes: + type: object + additionalProperties: false + properties: + name: + type: string + example: Harry + lastname: + type: string + example: Potter + real_photo: + type: + - string + - 'null' + example: https://i.ibb.co/C9LrC1j/harry-real.jpg + cartoon_photo: + type: + - string + - 'null' + example: https://i.ibb.co/cTv2VKK/harry-cartoon.jpg + ocupation: + type: + - string + - 'null' + example: Student + wand: + type: + - string + - 'null' + example: Holly, phoenix feather, 11" + patronus: + type: + - string + - 'null' + example: Stag + school_house: + type: + - string + - 'null' + example: Gryffindor + genre: + type: + - string + - 'null' + example: Male + required: + - name + - lastname + - real_photo + - cartoon_photo + - ocupation + - wand + - patronus + - school_house + - genre + + PersonResource: + type: object + additionalProperties: false + properties: + id: + type: string + example: '1' + type: + type: string + example: person + attributes: + $ref: '#/components/schemas/PersonAttributes' + required: + - id + - type + - attributes + + PersonResponse: + type: object + additionalProperties: false + properties: + data: + $ref: '#/components/schemas/PersonResource' + required: + - data + + PersonCollectionResponse: + type: object + additionalProperties: false + properties: + data: + type: array + items: + $ref: '#/components/schemas/PersonResource' + required: + - data + + CreatureAttributes: + type: object + additionalProperties: false + properties: + name: + type: string + example: Hippogriff + related_to: + type: + - string + - 'null' + example: Beast + skin_color: + type: + - string + - 'null' + example: Grey + eye_color: + type: + - string + - 'null' + example: Orange + mortality: + type: + - string + - 'null' + example: Mortal + img: + type: string + example: https://example.com/hippogriff.png + required: + - name + - related_to + - skin_color + - eye_color + - mortality + - img + + CreatureResource: + type: object + additionalProperties: false + properties: + id: + type: string + example: '1' + type: + type: string + example: creature + attributes: + $ref: '#/components/schemas/CreatureAttributes' + required: + - id + - type + - attributes + + CreatureResponse: + type: object + additionalProperties: false + properties: + data: + $ref: '#/components/schemas/CreatureResource' + required: + - data + + CreatureCollectionResponse: + type: object + additionalProperties: false + properties: + data: + type: array + items: + $ref: '#/components/schemas/CreatureResource' + required: + - data