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