diff --git a/.travis.yml b/.travis.yml index 2e06b75..4e844c0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -47,10 +47,31 @@ jobs: - curl -sSL https://get.haskellstack.org/ | sh # travis_retry works around https://github.com/commercialhaskell/stack/issues/4888 - travis_retry stack setup + # Decrypt the GitHub deploy key (travis_key.enc) + - openssl aes-256-cbc -k "$travis_key_password" -d -md sha256 -a -in travis_key.enc -out travis_key + - echo "Host github.com" > ~/.ssh/config + - echo " IdentityFile $(pwd)/travis_key" >> ~/.ssh/config + - chmod 400 travis_key + - git remote set-url origin git@github.com:aelve/guide.git script: # Build - stack --no-terminal build --test --no-run-tests --dependencies-only - stack --no-terminal build --test --no-run-tests + # Regenerate Swagger and push to the same branch, even if the branch + # is already ahead (which may happen if the previous build in the + # pipeline also pushed to it) + - | + if [ "$TRAVIS_EVENT_TYPE" = "push" ]; then + git checkout "$TRAVIS_BRANCH" && git pull + stack exec -- guide api-docs > back/swagger.json + git add back/swagger.json + # Will only push if the commit was created successfully. Note + # that we don't use "[skip ci]" in the commit message here + # because then Mergify goes "oh, but Travis hasn't passed, so I + # shouldn't merge this". + (git commit -m "Regenerate swagger.json" && git push) || true + git checkout "$TRAVIS_COMMIT" + fi # Upload the Docker image - | if [ "$TRAVIS_EVENT_TYPE" = "push" ]; then @@ -113,3 +134,9 @@ jobs: notifications: slack: secure: BgQpUYFmvXrf7HVBP/fefS/8UVwES800+fT+ufgJX8b2HMx2FvaWVsdv3ErKAryLE0B3fwmvforWugTdgLO3kq66YUgSt51SNQOBLkMVGubIoQsgvr3Ernu+Wpw1DyoMkXQH9q9O9rfCIc4IwkQCEHqu5SVRqdOd5px/CHFl/ktTI22JkT8ap/Be53qjlB2U2sWUf4GxYXq0V/gGF6fDwsUwTVKFb14RfSDrOgK5Vlce2GRf3gNr1C/j7A7EHIR/Z+rNd2hvv69cFw6TRc3s39QmP8XPe3SLZPIHTZ8vRveX1SZioMeEy747r5rHd9vylEjxWtVHhvP9fOt693+woXa8ZAl5uVRgB6S4mTWLZ+LAbqhaCmDGJYr9GrrBMoqWvJiMuBX3ZvHptsAc6O2l/fxZQU3otTE++SmHkhbyoDQkcPCjXPDUi/ZlnoLc5zfMAfApcsZZ8b9t47z12H0O4uDZd2YiNPiQJ1iUA6R879LH3pcxPB3RaoWsfXzv/klkKrU/V2K4SXD9j4/bmAFArlig+dar+Dm44L/a3/G7vbU1lQIa1bG0EqB36qgUS3UCkuy2ppti/JTHpkYx7HVF2BipoCjOVvfBl9G8RkvcQIhyuCfOGm7WL1TjrKVMccIEGJKhm7OO6wOZYCBfAI5zILxi8XEJAIvBm9NywhQlwxI= + +# travis_key.enc decryption password, see: +# http://markbucciarelli.com/posts/2019-01-26_how-to-push-to-github-from-travis-ci.html +env: + matrix: + secure: RqWbR4JjNKL5+KS6um6R2xKRGMxYO4qxRdI/RFi5oHsjRwKAQPGATb00Zmd84V/jaVb+NOccG1qp0/SNVwAaFf6Yr1MO9aV3GKC0fDFwSffKLdtVO2yx/JHrRYZ2ymrjEkBGug1FMrHFGZsSLaZYsYKx974tBs04xwAmPv6Yby8bZTGJyknbuyIMeSe6XZIkinyKaw/Zay6QC2CtSLoX50gqY2/HX0fVBgWKDNStigH0UEEynSetRYm/PFRcoBwu3XmHW89Y9B0E/zgvnvFuAgMVmZSUZnD9wBRlnFvfC1pUaFZP8Tz2VZhcJ4xTRTq+r3t2IRTNQBIhlQseobQMxoTrv1Y/ZiEmO14CLSwy3yeX5vCiGmCaA3957FqnjTP07svbru/A0qWMyEtuBtpheCVbQSVvMUBnrl0txTMFiBb4dsJ7zWrp3f7RQ8SFB11FwpGv89d+FifOpXN42DWRXjU0fLCPs8S5iKODWkTSQ41vpGXXUoZdaUOUg1y6tZSoc8gs61KlhcDTrBI2ZCJMNY6c6JVE/BOnJrp6zqyKhY2znyJUodEvjPsy9iccmrt0bEZTVshzbW4Q9okQ26usNtwIJoHNDUdifpmMcobb/ATYEr+C7n9ztxRy9AnZLzp6SsCDlfSDDp32fr7762PMk+2jR9w7fMrR/PBMtDXNsYQ= diff --git a/back/src/Guide/Api.hs b/back/src/Guide/Api.hs index d8f20d2..43eaa8b 100644 --- a/back/src/Guide/Api.hs +++ b/back/src/Guide/Api.hs @@ -3,9 +3,11 @@ module Guide.Api module Guide.Api.Methods, module Guide.Api.Server, module Guide.Api.Types, + module Guide.Api.Docs, ) where import Guide.Api.Methods import Guide.Api.Server import Guide.Api.Types +import Guide.Api.Docs diff --git a/back/src/Guide/Api/Docs.hs b/back/src/Guide/Api/Docs.hs new file mode 100644 index 0000000..76f7277 --- /dev/null +++ b/back/src/Guide/Api/Docs.hs @@ -0,0 +1,32 @@ +{-# LANGUAGE OverloadedStrings #-} + +-- | Rendered documentation for the API. +module Guide.Api.Docs +( + apiSwaggerDoc, + apiSwaggerRendered, +) +where + +import Imports + +import Servant.Swagger (toSwagger) +import Data.Swagger +import qualified Data.Aeson.Encode.Pretty as AesonPretty + +import Guide.Api.Types + +---------------------------------------------------------------------------- +-- Swagger +---------------------------------------------------------------------------- + +-- | Swagger docs for the 'Api'. +apiSwaggerDoc :: Swagger +apiSwaggerDoc = + toSwagger (Proxy @Api) + & info.title .~ "Aelve Guide API" + & info.version .~ "alpha" + +-- | Pretty-printed @swagger.json@ for the 'Api'. +apiSwaggerRendered :: Text +apiSwaggerRendered = utf8ToText $ AesonPretty.encodePretty apiSwaggerDoc diff --git a/back/src/Guide/Api/Server.hs b/back/src/Guide/Api/Server.hs index e17e6a5..29b054f 100644 --- a/back/src/Guide/Api/Server.hs +++ b/back/src/Guide/Api/Server.hs @@ -13,19 +13,18 @@ where import Imports -import Data.Swagger.Lens hiding (format) import Network.Wai (Middleware, Request) import Network.Wai.Middleware.Cors (CorsResourcePolicy (..), corsOrigins, simpleCorsResourcePolicy) import Servant import Servant.API.Generic import Servant.Server.Generic -import Servant.Swagger import Servant.Swagger.UI import Guide.Api.Guider import Guide.Api.Methods import Guide.Api.Types +import Guide.Api.Docs import Guide.Logger import Guide.Config import Guide.State @@ -76,11 +75,13 @@ logException logger mbReq ex = -- Servant servers ---------------------------------------------------------------------------- --- Collect API and Swagger server to united 'FullApi'. First takes precedence in case of overlap. +-- | Collect API and Swagger server to united 'FullApi'. First takes +-- precedence in case of overlap. fullServer :: DB -> Logger -> Config -> Server FullApi fullServer db di config = apiServer db di config :<|> docServer --- Collect api out of guiders and convert them to handlers. Type 'type Server api = ServerT api Handler' needed it. +-- | Collect api out of guiders and convert them to handlers. Type 'type +-- Server api = ServerT api Handler' needed it. apiServer :: DB -> Logger -> Config -> Server Api apiServer db di config = do requestDetails <- ask @@ -89,11 +90,7 @@ apiServer db di config = do -- | A 'Server' for Swagger docs. docServer :: Server (SwaggerSchemaUI "api" "swagger.json") -docServer = swaggerSchemaUIServer doc - where - doc = toSwagger (Proxy @Api) - & info.title .~ "Aelve Guide API" - & info.version .~ "alpha" +docServer = swaggerSchemaUIServer apiSwaggerDoc ---------------------------------------------------------------------------- -- API handlers put together ('Site') diff --git a/back/src/Guide/Cli.hs b/back/src/Guide/Cli.hs index d3f4fd3..7ffb907 100644 --- a/back/src/Guide/Cli.hs +++ b/back/src/Guide/Cli.hs @@ -24,42 +24,10 @@ import qualified Options.Applicative as Opt -- | All available commands data Command - = RunServer -- ^ run server - | DryRun -- ^ load database and exit - | LoadPublic FilePath -- ^ load PublicDB, create base on it and exit - ----------------------------------------------------------------------------- --- Parsers ----------------------------------------------------------------------------- - -{- -To see help run command: -$ guide --help -Usage: guide [-v|--version] [COMMAND] - -Available options: - -h,--help Show this help text - -v,--version Show Guide version - -Available commands: - run Run server - dry-run Load database and exit - load-public Load PublicDB, create base on it and exit - -NOTE: -Command 'guide' is the same as 'guide run' - ----------------------------------------------------------------------------- - -$ guide load-public --help -Usage: guide load-public (-p|--path FILEPATH) - Load PublicDB, create base on it and exit - -Available options: - -h,--help Show this help text - -p,--path FILEPATH Public DB file name - --} + = RunServer -- ^ Run server + | DryRun -- ^ Load database and exit + | LoadPublic FilePath -- ^ Load PublicDB, create base on it and exit + | ApiDocs -- ^ Show docs for the backend API -- | Parse the command line of the application. -- @@ -72,10 +40,14 @@ parseCommandLine = Opt.execParser -- | All possible commands. commandsParser :: Parser Command commandsParser = Opt.subparser - $ Opt.command "run" (infoP (pure RunServer) "Start server") - <> Opt.command "dry-run" (infoP (pure DryRun) "Load database and exit") + $ Opt.command "run" + (infoP (pure RunServer) "Start server") + <> Opt.command "dry-run" + (infoP (pure DryRun) "Load database and exit") <> Opt.command "load-public" (infoP loadPublicParser "Load PublicDB, create base on it and exit") + <> Opt.command "api-docs" + (infoP (pure ApiDocs) "Show swagger.json for the backend API") where infoP parser desc = Opt.info (Opt.helper <*> parser) $ Opt.progDesc desc diff --git a/back/src/Guide/Config.hs b/back/src/Guide/Config.hs index 0a6b758..e367379 100644 --- a/back/src/Guide/Config.hs +++ b/back/src/Guide/Config.hs @@ -16,11 +16,10 @@ where import Imports hiding ((.=)) --- JSON import Data.Aeson as Aeson import Data.Aeson.Encode.Pretty as Aeson hiding (Config) --- Default import Data.Default +import Say (sayErr) import Guide.Utils @@ -125,7 +124,7 @@ readConfig = do let filename = "config.json" exists <- doesFileExist filename unless exists $ do - putStrLn "config.json doesn't exist, creating it" + sayErr "config.json doesn't exist, creating it" BSL.writeFile filename (Aeson.encodePretty (def :: Config)) contents <- toLazyByteString <$> BS.readFile filename case Aeson.eitherDecode' contents of diff --git a/back/src/Guide/Main.hs b/back/src/Guide/Main.hs index 50045bc..6f7fcff 100644 --- a/back/src/Guide/Main.hs +++ b/back/src/Guide/Main.hs @@ -15,6 +15,7 @@ module Guide.Main runServer, dryRun, loadPublic, + apiDocs, ) where @@ -47,7 +48,7 @@ import System.Signal -- HVect import Data.HVect hiding (length) -import Guide.Api (runApiServer) +import Guide.Api (runApiServer, apiSwaggerRendered) import Guide.App import Guide.Cli import Guide.Config @@ -124,6 +125,7 @@ runCommand config = \case RunServer -> runServer config DryRun -> dryRun config LoadPublic path -> loadPublic config path + ApiDocs -> apiDocs config ---------------------------------------------------------------------------- -- Commands @@ -167,6 +169,11 @@ loadPublic config path = withLogger config $ \logger -> logDebugIO logger "PublicDB imported to GlobalState" exitSuccess +-- | Dump API docs to the output. +apiDocs :: Config -> IO () +apiDocs config = withLogger config $ \_logger -> + T.putStrLn apiSwaggerRendered + ---------------------------------------------------------------------------- -- Helpers ---------------------------------------------------------------------------- diff --git a/back/swagger.json b/back/swagger.json new file mode 100644 index 0000000..82fcf27 --- /dev/null +++ b/back/swagger.json @@ -0,0 +1,1211 @@ +{ + "swagger": "2.0", + "info": { + "version": "alpha", + "title": "Aelve Guide API" + }, + "definitions": { + "CategoryStatus": { + "type": "string", + "enum": [ + "CategoryStub", + "CategoryWIP", + "CategoryFinished" + ] + }, + "CItemInfoEdit": { + "type": "object", + "properties": { + "hackage": { + "type": "string", + "description": "Package name on Hackage" + }, + "link": { + "type": "string", + "description": "Link to the official site, if exists" + }, + "name": { + "type": "string", + "description": "Item name" + } + } + }, + "TraitID": { + "type": "string" + }, + "CMove": { + "required": [ + "direction" + ], + "type": "object", + "properties": { + "direction": { + "$ref": "#/definitions/CDirection" + } + } + }, + "CSRCategory": { + "required": [ + "info", + "description" + ], + "type": "object", + "properties": { + "description": { + "$ref": "#/definitions/CMarkdown" + }, + "info": { + "$ref": "#/definitions/CCategoryInfo" + } + } + }, + "CCreateTrait": { + "required": [ + "type", + "content" + ], + "type": "object", + "properties": { + "content": { + "type": "string" + }, + "type": { + "$ref": "#/definitions/CTraitType" + } + } + }, + "CCategoryFull": { + "required": [ + "id", + "title", + "group", + "status", + "description", + "sections", + "items" + ], + "type": "object", + "properties": { + "status": { + "$ref": "#/definitions/CategoryStatus" + }, + "group": { + "type": "string", + "description": "Category group ('grandcategory')" + }, + "items": { + "items": { + "$ref": "#/definitions/CItemFull" + }, + "type": "array", + "description": "All items in the category" + }, + "sections": { + "uniqueItems": true, + "items": { + "$ref": "#/definitions/ItemSection" + }, + "type": "array", + "description": "Enabled item sections" + }, + "id": { + "$ref": "#/definitions/CategoryID" + }, + "title": { + "type": "string", + "description": "Category title" + }, + "description": { + "$ref": "#/definitions/CMarkdown" + } + } + }, + "CItemInfo": { + "required": [ + "id", + "created", + "name" + ], + "type": "object", + "properties": { + "hackage": { + "type": "string", + "description": "Package name on Hackage" + }, + "created": { + "example": "2016-07-22T00:00:00Z", + "format": "yyyy-mm-ddThh:MM:ssZ", + "type": "string", + "description": "When the item was created" + }, + "link": { + "type": "string", + "description": "Link to the official site, if exists" + }, + "name": { + "type": "string", + "description": "Item name" + }, + "id": { + "$ref": "#/definitions/ItemID" + } + } + }, + "ItemID": { + "type": "string" + }, + "CTraitType": { + "type": "string", + "enum": [ + "Pro", + "Con" + ] + }, + "CSearchResult": { + "minProperties": 1, + "maxProperties": 1, + "type": "object", + "description": "The docs lie. The true schema for this type is an object with two parameters 'tag' and 'contents', where 'tag' is one of keys listed in this doc, and 'contents' is the object.", + "properties": { + "Category": { + "$ref": "#/definitions/CSRCategory" + }, + "Item": { + "$ref": "#/definitions/CSRItem" + } + } + }, + "CMarkdown": { + "required": [ + "text", + "html" + ], + "type": "object", + "properties": { + "text": { + "type": "string", + "description": "Markdown source" + }, + "html": { + "type": "string", + "description": "Rendered HTML" + } + } + }, + "ItemSection": { + "type": "string", + "enum": [ + "ItemProsConsSection", + "ItemEcosystemSection", + "ItemNotesSection" + ] + }, + "CSRItem": { + "required": [ + "category", + "info" + ], + "type": "object", + "description": "Note: fields `summary` and `ecosystem` will be present only if the match was found in those fields.", + "properties": { + "ecosystem": { + "$ref": "#/definitions/CMarkdown" + }, + "summary": { + "$ref": "#/definitions/CMarkdown" + }, + "category": { + "$ref": "#/definitions/CCategoryInfo" + }, + "info": { + "$ref": "#/definitions/CItemInfo" + } + } + }, + "UTCTime": { + "example": "2016-07-22T00:00:00Z", + "format": "yyyy-mm-ddThh:MM:ssZ", + "type": "string" + }, + "CTextEdit": { + "required": [ + "original", + "modified" + ], + "type": "object", + "properties": { + "modified": { + "type": "string", + "description": "Modified text" + }, + "original": { + "type": "string", + "description": "State of base before editing" + } + } + }, + "CCreateItem": { + "required": [ + "name" + ], + "type": "object", + "properties": { + "hackage": { + "type": "string", + "description": "Package name on Hackage" + }, + "link": { + "type": "string", + "description": "Link to the official site, if exists" + }, + "name": { + "type": "string", + "description": "Item name" + } + } + }, + "CItemFull": { + "required": [ + "id", + "name", + "created", + "summary", + "pros", + "cons", + "ecosystem", + "notes", + "toc" + ], + "type": "object", + "properties": { + "ecosystem": { + "$ref": "#/definitions/CMarkdown" + }, + "summary": { + "$ref": "#/definitions/CMarkdown" + }, + "hackage": { + "type": "string", + "description": "Package name on Hackage" + }, + "created": { + "example": "2016-07-22T00:00:00Z", + "format": "yyyy-mm-ddThh:MM:ssZ", + "type": "string", + "description": "When the item was created" + }, + "link": { + "type": "string", + "description": "Link to the official site, if exists" + }, + "toc": { + "items": { + "$ref": "#/definitions/CTocHeading" + }, + "type": "array", + "description": "Table of contents" + }, + "name": { + "type": "string", + "description": "Item name" + }, + "id": { + "$ref": "#/definitions/ItemID" + }, + "notes": { + "$ref": "#/definitions/CMarkdown" + }, + "pros": { + "items": { + "$ref": "#/definitions/CTrait" + }, + "type": "array", + "description": "Pros (positive traits)" + }, + "cons": { + "items": { + "$ref": "#/definitions/CTrait" + }, + "type": "array", + "description": "Cons (negative traits)" + } + } + }, + "CategoryID": { + "type": "string" + }, + "CCategoryInfo": { + "required": [ + "id", + "title", + "created", + "group", + "status" + ], + "type": "object", + "properties": { + "status": { + "$ref": "#/definitions/CategoryStatus" + }, + "group": { + "type": "string", + "description": "Category group ('grandcategory')" + }, + "created": { + "example": "2016-07-22T00:00:00Z", + "format": "yyyy-mm-ddThh:MM:ssZ", + "type": "string", + "description": "When the category was created" + }, + "id": { + "$ref": "#/definitions/CategoryID" + }, + "title": { + "type": "string", + "description": "Category title" + } + } + }, + "CTocHeading": { + "required": [ + "content", + "slug", + "subheadings" + ], + "type": "object", + "properties": { + "slug": { + "type": "string", + "description": "In-page anchor for linking" + }, + "content": { + "$ref": "#/definitions/CMarkdown" + }, + "subheadings": { + "items": { + "$ref": "#/definitions/CTocHeading" + }, + "type": "array" + } + } + }, + "CDirection": { + "type": "string", + "enum": [ + "up", + "down" + ] + }, + "CCategoryInfoEdit": { + "required": [ + "title", + "group", + "status", + "sections" + ], + "type": "object", + "properties": { + "status": { + "$ref": "#/definitions/CategoryStatus" + }, + "group": { + "type": "string", + "description": "Category group ('grandcategory')" + }, + "sections": { + "uniqueItems": true, + "items": { + "$ref": "#/definitions/ItemSection" + }, + "type": "array", + "description": "Enabled item sections" + }, + "title": { + "type": "string", + "description": "Category title" + } + } + }, + "CTrait": { + "required": [ + "id", + "content" + ], + "type": "object", + "properties": { + "content": { + "$ref": "#/definitions/CMarkdown" + }, + "id": { + "$ref": "#/definitions/TraitID" + } + } + } + }, + "paths": { + "/item/{itemId}/info": { + "put": { + "summary": "Set item's info", + "consumes": [ + "application/json;charset=utf-8" + ], + "responses": { + "404": { + "description": "Item not found" + }, + "200": { + "description": "" + } + }, + "produces": [ + "application/json;charset=utf-8" + ], + "parameters": [ + { + "format": "Item ID", + "required": true, + "in": "path", + "name": "itemId", + "type": "string" + }, + { + "required": true, + "schema": { + "$ref": "#/definitions/CItemInfoEdit" + }, + "in": "body", + "name": "body" + } + ], + "description": "Note: all fields are optional. If you don't pass a field, it won't be modified. To erase a field, send `null`.", + "tags": [ + "02. Items" + ] + } + }, + "/category/{categoryId}/info": { + "put": { + "summary": "Set category's fields", + "consumes": [ + "application/json;charset=utf-8" + ], + "responses": { + "404": { + "description": "Category not found" + }, + "200": { + "description": "" + } + }, + "produces": [ + "application/json;charset=utf-8" + ], + "parameters": [ + { + "format": "Category ID", + "required": true, + "in": "path", + "name": "categoryId", + "type": "string" + }, + { + "required": true, + "schema": { + "$ref": "#/definitions/CCategoryInfoEdit" + }, + "in": "body", + "name": "body" + } + ], + "tags": [ + "01. Categories" + ] + } + }, + "/item/{itemId}/summary": { + "put": { + "summary": "Set item's summary", + "consumes": [ + "application/json;charset=utf-8" + ], + "responses": { + "409": { + "description": "Merge conflict occurred" + }, + "404": { + "description": "Item not found" + }, + "200": { + "description": "" + } + }, + "produces": [ + "application/json;charset=utf-8" + ], + "parameters": [ + { + "format": "Item ID", + "required": true, + "in": "path", + "name": "itemId", + "type": "string" + }, + { + "required": true, + "schema": { + "$ref": "#/definitions/CTextEdit" + }, + "in": "body", + "name": "body" + } + ], + "tags": [ + "02. Items" + ] + } + }, + "/item/{itemId}/trait/{traitId}": { + "get": { + "summary": "Get trait by id", + "responses": { + "404": { + "description": "Item not found" + }, + "200": { + "schema": { + "$ref": "#/definitions/CTrait" + }, + "description": "" + } + }, + "produces": [ + "application/json;charset=utf-8" + ], + "parameters": [ + { + "format": "Item ID", + "required": true, + "in": "path", + "name": "itemId", + "type": "string" + }, + { + "format": "Trait ID", + "required": true, + "in": "path", + "name": "traitId", + "type": "string" + } + ], + "tags": [ + "03. Item traits" + ] + }, + "delete": { + "summary": "Delete a trait", + "responses": { + "404": { + "description": "Item not found" + }, + "200": { + "description": "" + } + }, + "produces": [ + "application/json;charset=utf-8" + ], + "parameters": [ + { + "format": "Item ID", + "required": true, + "in": "path", + "name": "itemId", + "type": "string" + }, + { + "format": "Trait ID", + "required": true, + "in": "path", + "name": "traitId", + "type": "string" + } + ], + "tags": [ + "03. Item traits" + ] + }, + "put": { + "summary": "Update a trait in the given item", + "consumes": [ + "application/json;charset=utf-8" + ], + "responses": { + "409": { + "description": "Merge conflict occurred" + }, + "404": { + "description": "Item not found" + }, + "200": { + "description": "" + } + }, + "produces": [ + "application/json;charset=utf-8" + ], + "parameters": [ + { + "format": "Item ID", + "required": true, + "in": "path", + "name": "itemId", + "type": "string" + }, + { + "format": "Trait ID", + "required": true, + "in": "path", + "name": "traitId", + "type": "string" + }, + { + "required": true, + "schema": { + "$ref": "#/definitions/CTextEdit" + }, + "in": "body", + "name": "body" + } + ], + "tags": [ + "03. Item traits" + ] + } + }, + "/item/{itemId}/ecosystem": { + "put": { + "summary": "Set item's ecosystem", + "consumes": [ + "application/json;charset=utf-8" + ], + "responses": { + "409": { + "description": "Merge conflict occurred" + }, + "404": { + "description": "Item not found" + }, + "200": { + "description": "" + } + }, + "produces": [ + "application/json;charset=utf-8" + ], + "parameters": [ + { + "format": "Item ID", + "required": true, + "in": "path", + "name": "itemId", + "type": "string" + }, + { + "required": true, + "schema": { + "$ref": "#/definitions/CTextEdit" + }, + "in": "body", + "name": "body" + } + ], + "tags": [ + "02. Items" + ] + } + }, + "/item/{itemId}": { + "get": { + "summary": "Get item by id", + "responses": { + "404": { + "description": "Item not found" + }, + "200": { + "schema": { + "$ref": "#/definitions/CItemFull" + }, + "description": "" + } + }, + "produces": [ + "application/json;charset=utf-8" + ], + "parameters": [ + { + "format": "Item ID", + "required": true, + "in": "path", + "name": "itemId", + "type": "string" + } + ], + "tags": [ + "02. Items" + ] + }, + "delete": { + "summary": "Delete an item", + "responses": { + "404": { + "description": "Item not found" + }, + "200": { + "description": "" + } + }, + "produces": [ + "application/json;charset=utf-8" + ], + "parameters": [ + { + "format": "Item ID", + "required": true, + "in": "path", + "name": "itemId", + "type": "string" + } + ], + "tags": [ + "02. Items" + ] + } + }, + "/category/{categoryId}": { + "get": { + "summary": "Get contents of a category", + "responses": { + "404": { + "description": "Category not found" + }, + "200": { + "schema": { + "$ref": "#/definitions/CCategoryFull" + }, + "description": "" + } + }, + "produces": [ + "application/json;charset=utf-8" + ], + "parameters": [ + { + "format": "Category ID", + "required": true, + "in": "path", + "name": "categoryId", + "type": "string" + } + ], + "tags": [ + "01. Categories" + ] + }, + "delete": { + "summary": "Delete a category", + "responses": { + "404": { + "description": "Category not found" + }, + "200": { + "description": "" + } + }, + "produces": [ + "application/json;charset=utf-8" + ], + "parameters": [ + { + "format": "Category ID", + "required": true, + "in": "path", + "name": "categoryId", + "type": "string" + } + ], + "tags": [ + "01. Categories" + ] + } + }, + "/item/{itemId}/trait": { + "post": { + "summary": "Create a new trait in the given item", + "consumes": [ + "application/json;charset=utf-8" + ], + "responses": { + "400": { + "description": "'content' can not be empty" + }, + "200": { + "schema": { + "$ref": "#/definitions/TraitID" + }, + "description": "" + } + }, + "produces": [ + "application/json;charset=utf-8" + ], + "parameters": [ + { + "format": "Item ID", + "required": true, + "in": "path", + "name": "itemId", + "type": "string" + }, + { + "required": true, + "schema": { + "$ref": "#/definitions/CCreateTrait" + }, + "in": "body", + "name": "body" + } + ], + "description": "Returns the ID of the created trait.", + "tags": [ + "03. Item traits" + ] + } + }, + "/categories": { + "get": { + "summary": "Get a list of available categories", + "responses": { + "200": { + "schema": { + "items": { + "$ref": "#/definitions/CCategoryInfo" + }, + "type": "array" + }, + "description": "" + } + }, + "produces": [ + "application/json;charset=utf-8" + ], + "description": "Primarily useful for displaying the main page. The returned list is lightweight and doesn't contain categories' contents.", + "tags": [ + "01. Categories" + ] + } + }, + "/category": { + "post": { + "summary": "Create a new category", + "responses": { + "400": { + "description": "'title' not provided" + }, + "200": { + "schema": { + "$ref": "#/definitions/CategoryID" + }, + "description": "" + } + }, + "produces": [ + "application/json;charset=utf-8" + ], + "parameters": [ + { + "required": true, + "in": "query", + "name": "title", + "type": "string", + "description": "Title of the newly created category" + }, + { + "required": true, + "in": "query", + "name": "group", + "type": "string", + "description": "Group to put the category into" + } + ], + "description": "Returns the ID of the created category.", + "tags": [ + "01. Categories" + ] + } + }, + "/item/{itemId}/notes": { + "put": { + "summary": "Set item's notes", + "consumes": [ + "application/json;charset=utf-8" + ], + "responses": { + "409": { + "description": "Merge conflict occurred" + }, + "404": { + "description": "Item not found" + }, + "200": { + "description": "" + } + }, + "produces": [ + "application/json;charset=utf-8" + ], + "parameters": [ + { + "format": "Item ID", + "required": true, + "in": "path", + "name": "itemId", + "type": "string" + }, + { + "required": true, + "schema": { + "$ref": "#/definitions/CTextEdit" + }, + "in": "body", + "name": "body" + } + ], + "tags": [ + "02. Items" + ] + } + }, + "/category/{categoryId}/notes": { + "put": { + "summary": "Edit category's notes", + "consumes": [ + "application/json;charset=utf-8" + ], + "responses": { + "409": { + "description": "Merge conflict occurred" + }, + "404": { + "description": "Category not found" + }, + "200": { + "description": "" + } + }, + "produces": [ + "application/json;charset=utf-8" + ], + "parameters": [ + { + "format": "Category ID", + "required": true, + "in": "path", + "name": "categoryId", + "type": "string" + }, + { + "required": true, + "schema": { + "$ref": "#/definitions/CTextEdit" + }, + "in": "body", + "name": "body" + } + ], + "tags": [ + "01. Categories" + ] + } + }, + "/item/{itemId}/move": { + "post": { + "summary": "Move item", + "consumes": [ + "application/json;charset=utf-8" + ], + "responses": { + "404": { + "description": "Item not found" + }, + "200": { + "description": "" + } + }, + "produces": [ + "application/json;charset=utf-8" + ], + "parameters": [ + { + "format": "Item ID", + "required": true, + "in": "path", + "name": "itemId", + "type": "string" + }, + { + "required": true, + "schema": { + "$ref": "#/definitions/CMove" + }, + "in": "body", + "name": "body" + } + ], + "tags": [ + "02. Items" + ] + } + }, + "/item/{itemId}/trait/{traitId}/move": { + "post": { + "summary": "Move trait", + "consumes": [ + "application/json;charset=utf-8" + ], + "responses": { + "404": { + "description": "Item not found" + }, + "200": { + "description": "" + } + }, + "produces": [ + "application/json;charset=utf-8" + ], + "parameters": [ + { + "format": "Item ID", + "required": true, + "in": "path", + "name": "itemId", + "type": "string" + }, + { + "format": "Trait ID", + "required": true, + "in": "path", + "name": "traitId", + "type": "string" + }, + { + "required": true, + "schema": { + "$ref": "#/definitions/CMove" + }, + "in": "body", + "name": "body" + } + ], + "tags": [ + "03. Item traits" + ] + } + }, + "/item/{categoryId}": { + "post": { + "summary": "Create a new item in the given category", + "consumes": [ + "application/json;charset=utf-8" + ], + "responses": { + "400": { + "description": "'name' can not be empty" + }, + "200": { + "schema": { + "$ref": "#/definitions/ItemID" + }, + "description": "" + } + }, + "produces": [ + "application/json;charset=utf-8" + ], + "parameters": [ + { + "format": "Category ID", + "required": true, + "in": "path", + "name": "categoryId", + "type": "string" + }, + { + "required": true, + "schema": { + "$ref": "#/definitions/CCreateItem" + }, + "in": "body", + "name": "body" + } + ], + "description": "Returns the ID of the created item.", + "tags": [ + "02. Items" + ] + } + }, + "/search": { + "get": { + "summary": "Search categories and items", + "responses": { + "400": { + "description": "'query' not provided" + }, + "200": { + "schema": { + "items": { + "$ref": "#/definitions/CSearchResult" + }, + "type": "array" + }, + "description": "" + } + }, + "produces": [ + "application/json;charset=utf-8" + ], + "parameters": [ + { + "required": true, + "in": "query", + "name": "query", + "type": "string" + } + ], + "description": "Note: returns at most 100 search results.", + "tags": [ + "04. Search" + ] + } + } + }, + "tags": [ + { + "name": "01. Categories", + "description": "Working with categories." + }, + { + "name": "02. Items", + "description": "Working with items." + }, + { + "name": "03. Item traits", + "description": "Working with item traits." + }, + { + "name": "04. Search", + "description": "Site-wide search." + } + ] +} diff --git a/travis_key.enc b/travis_key.enc new file mode 100644 index 0000000..d907184 --- /dev/null +++ b/travis_key.enc @@ -0,0 +1,71 @@ +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