Skip to content

Proposal API contract #7962

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 10 commits into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
134 changes: 134 additions & 0 deletions docs/APIContract.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
# Versions

So far there is only one version

The base URL for an API should be: _baseURL_/_apiVersion_/ meaning even something such as sometestbuildsforpoe.gg/something/a/b/c/ would be valid as a base URL, as long as it has afterwards the per version required endpoints.

## Metadata Endpoint

In order to provide a consistent API a metadata endpoint should be provided under _baseURL_/.well-known/pathofbuilding this endpoint should provide information about the API and the version of the API.
Furthermore this may allow having custom endpoints for the API.

<details><summary><b>Specification</b></summary>
| Feature | Field | Type | Description |
| ---------------- | ------------- | ---------------- | ------------------------------------------------------------------------------------ |
| League Filtering | league_filter | bool | This can be used to indicate whether the API supports filtering based on leagues |
| Gem Filtering | gem_filter | bool | This can be used to indicate whether the API supports filtering based on gems |
| Streams | streams | StreamInfo[] | A list of streams available to be queried against |
| Update Builds | update_builds | UpdateBuildsInfo | Indicates if the API supports updating builds via PoB and which fields are supported |

</details>

### Types

<details><summary><b>StreamInfo</b></summary>

| Field | Type | Description |
| ------- | ------ | ------------------------------- |
| name | string | Name of the stream |
| apiPath | string | API path to the stream endpoint |

apiPath might be changed to a generic endpoint such as `/v1/{stream}/builds`
</details>

<details><summary><b>UpdateBuildsInfo</b></summary>
| Field | Type | Description |
| ---------- | -------- | ------------------------------------------------------ |
| hasSupport | bool | indicates if the API supports updating external builds |
| fields | string[] | list of fields that can be updated |

Example:

```json
{
"hasSupport": true,
"fields": ["description", "youtubeUrl"]
}
```
</details>

## Version 1

### Response and Request types

<details><summary><b>BuildInfo</b></summary>

| Field | Type | Description |
| ------------ | ------- | ----------------------------------- |
| pobdata | string | The Path of Building data |
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does this contain, the build code?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes or rather the standard pob code used for sharing builds, so I assume the description should make this more clear?

| name | string | The name of the build |
| lastModified | integer | The last modification timestamp |
| buildId | string | The unique identifier for the build |
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unique identifier on the website? Is that a link or just an internal id?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be an unique identifier as it could be used for updating a local cache for said build, it doesn't have to be an internal id to either pob nor the api but it needs to map to exactly one build.

As going further there may be an option long-term to sync builds from sources to PoB and updating with the click of a button instead of manually reimporting.

tl;dr: Unique Identifier provided by source to map to one specific build

</details>

### Endpoints

<details>
<summary><code>GET</code> <code><b>/v1/builds</b></code> <code>(Lists multiple builds)</code></summary>

#### Parameters
> Query Parameters

| name | type | data type | description |
| ------ | -------- | --------- | --------------------------------------- |
| page | optional | integer | Used for pagination |
| league | optional | string | Used to limit builds to a league |
| gems | optional | string | Used to limit builds with specific gems |

##### League

These values will just be the base patch version for the league.

These links are generally available via poewiki see: https://www.poewiki.net/wiki/Necropolis_league _Official Page_.

Example values:

| Patch | League | value | url |
| ----- | ---------------------- | ----- | -------------------------------------- |
| 3.25 | Settlers of Kalguur | 3.25 | https://www.pathofexile.com/settlers |
| 3.24 | Necropolis | 3.24 | https://www.pathofexile.com/necropolis |
| 3.23 | Affliction | 3.23 | https://www.pathofexile.com/affliction |
| 3.22 | Trial of the Ancestors | 3.22 | https://www.pathofexile.com/ancestor |
| 3.4 | Delve | 3.4 | https://www.pathofexile.com/delve |

#### Responses

| http code | content-type | response |
| --------- | ------------------ | ------------------ |
| `200` | `application/json` | `BuildInfo object` |
Copy link
Contributor

@Dav1dde Dav1dde Jul 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We probably need more metadata, for example a description/name of the list returned (or multiple?). pobarchives may provide a list of recent builds, pobbin would only provide popular.

This need goes away if every 'provider' can only provide a single list, but currently more is possible.

It must also be possible for the page to return if there are more pages available or at least indicate this was the last page.

Copy link
Author

@RaiNote RaiNote Jul 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So possibly a wrapper object such as:

{
  "totalPages": 100,
  "buildInfos": [{}],
}

something like this could then be used for assumption based "this is the last page".
Alas I'm still thinking about how multiple sources should be displayed together as in if every source has 10 entries and I have 100 sources I would get 1000 builds in technically one PoB page (ignoring that local pagination simulation is possible).


</details>


<details>
<summary><code>POST</code> <code><b>/v1/builds/{buildId}</b></code> <code>(Update a single build on the source)</code></summary>

#### Parameters
> Path Parameters

| name | type | data type | description |
| ------- | -------- | --------- | ---------------------------------------------------------- |
| buildId | required | string | The build in question on the source that should be updated |

#### Request

| Field | Type | Description |
| ---------- | -------- | ---------------------------------- |
| pobdata | string | The Path of Building data |
| name | string | The name of the build |
| customData | string[] | A list of custom data to be stored |

customData will be describable fields via the metadata endpoint, if customData is empty it is expected to be ignored and no changes should be made.

#### Responses

| http code | content-type | response | Description |
| --------- | -------------------------- | --------------------- | ---------------------- |
| `200` | `application/json` | None | Update succesful |
| `201` | `application/json` | None | Succesful creation |
| `400` | `text/html; charset=utf-8` | `Reason for failure` | Input may be incorrect |
| `401` | `text/html; charset=utf-8` | `Reason if necessary` | Auth incorrect |
| `404` | `text/html; charset=utf-8` | None | Build does not exist |

</details>

203 changes: 203 additions & 0 deletions src/Classes/APIContractBuilds.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
-- Path of Building
--
-- Class: API Contract Builds
-- API contract proposal for builds
--

local dkjson = require "dkjson"


---@enum EndpointType
local EndpointType = {
REST = 0,
CSV = 1
}

--example for REST vs CSV
-- REST:
-- {"baseUrl": "https://mypoebuilds.gg/pob-api/", "name": "MyPoEBuilds REST", "endpointType": 0, "fallbackVersion": 1}
-- baseUrl meaning the actual baseUrl
-- CSV:
-- {"baseUrl": "https://mypoebuilds.gg/pob-builds.csv", "name": "MyPoEBuilds CSV", "endpointType": 1, "fallbackVersion": 1}
-- baseUrl means here the full URL to the actual file or endpoint that returns a CSV formatted file from a GET endpoint

--- APISourceInfo is the configurated data for a source from the enduser
---@class APISourceInfo
---@field name string
---@field baseUrl string
---@field endpointType EndpointType
---@field fallbackVersion integer

--- This data should be returned by the source
---@class BuildInfo
---@field pobdata string
---@field name string
---@field lastModified integer
---@field buildId string

--- This data is saved internally for caching
--- Save this to disk for temporary local caching if required/desired
---@class BuildInfoCache
---@field pobdata string
---@field name string
---@field lastModified integer
---@field buildId string
---@field sourceName string

--- API Capabilities returned by the source or defaulted to APISourceInfo
---@class APICapabilities
---@field name string
---@field fallbackVersion integer
---@field endpointType EndpointType
---@field supportedVersion? integer
---@field baseAPIPath? string
---@field league_filter? boolean
---@field gem_filter? boolean

--- Build version 1 Filter option
---@class BuildVersion1Filter
---@field league string
---@field gem string

--- This primarily exists for the lua language server
---@param buildInfo BuildInfo
---@param source APICapabilities
---@return BuildInfoCache
local function buildInfoToCache(buildInfo, source)
return {
pobdata = buildInfo.pobdata,
name = buildInfo.name,
lastModified = buildInfo.lastModified,
buildId = buildInfo.buildId,
sourceName = source.name
}
end

---@param t table
local function tableToQueryParams(t)
if #t == 0 then
return ""
end
local query = "?"
for key, value in pairs(t) do
if value then
local tempValue = value
if type(value) == "table" then
tempValue = table.concat(value, ",")
end
query = query .. key .. "=" .. value .. "&"
end
end
-- remove trailing &
if #t > 0 then
query = query:sub(1, #query - 1)
end

return query
end

---@class APIContractBuilds
---@field buildList table<string,BuildInfoCache>
---@field apiCapabilities table<string, APICapabilities>
local APIContractBuilds = newClass("APIContractBuilds",
function(self)
self.buildList = {}
self.apiCapabilities = {}
end
)

-- Switch case for GET Endpoint for Builds
local getBuildVersions = {
[1] = function(...) return APIContractBuilds:GetBuildsVersion1(...) end,
}
-- Switch case for GET Endpoint for Builds
local getBuildFilterVersions = {
[1] = function(data) return APIContractBuilds:GetBuildVersion1Filter(data) end,
}

---@param data table
function APIContractBuilds:GetBuildVersion1Filter(data)
return {
league = data.league,
gem = data.gem
}
end

--- Gets the builds from the source
---@param source APICapabilities
---@param data table
function APIContractBuilds:GetBuilds(source, data)
local getBuildsFunction = nil
local getBuildsFilterFunction = nil
if not source.supportedVersion then
getBuildsFunction = getBuildVersions[source.fallbackVersion]
getBuildsFilterFunction = getBuildFilterVersions[source.fallbackVersion]
else
getBuildsFunction = getBuildVersions[source.supportedVersion]
getBuildsFilterFunction = getBuildFilterVersions[source.supportedVersion]
end

if getBuildsFunction and getBuildsFilterFunction then
getBuildsFunction(source.endpointType, getBuildsFilterFunction(data), source)
end
end

---@param source APISourceInfo
function APIContractBuilds:UpdateAPICapabilities(source)
-- TODO: Which features are supported?
-- What is the latest version that is supported?
-- Which endpoints are supported
-- Is Querying supported? (CSV won't support this probably)

if source.endpointType ~= EndpointType.REST then
return
end

launch:DownloadPage(source.baseUrl + "/.well-known/pathofbuilding",
function(...) return APIContractBuilds:APICapabilitiesCallback(source, ...) end, {})
end

--- Updates the API capabilities for the source
---@param response table
---@param errMsg any
---@param source APISourceInfo
function APIContractBuilds:APICapabilitiesCallback(source, response, errMsg)
local parsedResponse = dkjson.decode(response.body)
---@cast parsedResponse APICapabilities
if errMsg or not parsedResponse.baseAPIPath then
parsedResponse.baseAPIPath = source.baseUrl
end
parsedResponse.name = source.name
parsedResponse.fallbackVersion = source.fallbackVersion
parsedResponse.endpointType = source.endpointType

self.apiCapabilities[source.name] = parsedResponse
end

--- Adds/Updates Builds for the source based on version 1
---@param response table
---@param errMsg any
---@param source APICapabilities
function APIContractBuilds:BuildsVersion1Callback(source, response, errMsg)
local parsedResponse = dkjson.decode(response.body)
---@cast parsedResponse BuildInfo[]
for _, build in ipairs(parsedResponse) do
-- source and buildId will be used as a caching key
-- to avoid buildId collissions
self.buildList[common.sha1(source.name .. "-" .. build.buildId)] = buildInfoToCache(build, source)
end
end

--- Version 1 of the API Contract for Builds
---@param source APICapabilities
---@param params BuildVersion1Filter
function APIContractBuilds:GetBuildsVersion1(source, params)
if source.endpointType == EndpointType.CSV then
-- TODO: Implement CSV Parsing and Format
return
end

launch:DownloadPage(source.baseAPIPath .. "/v1/builds" .. tableToQueryParams(params), function(...)
self:BuildsVersion1Callback(source, ...)
end, {})
end