├── .gitignore ├── .travis.yml ├── Dockerfile ├── Gopkg.lock ├── Gopkg.toml ├── LICENSE ├── Makefile ├── README.md ├── go.mod ├── go.sum ├── handlers.go ├── handlers_test.go ├── logger.go ├── logger_test.go ├── main.go ├── md2json ├── rotate-log ├── util.go └── util_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | public-api 3 | *.exe 4 | *.exe~ 5 | *.dll 6 | *.so 7 | *.dylib 8 | 9 | # Test binary, build with `go test -c` 10 | *.test 11 | 12 | # Output of the go coverage tool, specifically when used with LiteIDE 13 | coverage.txt 14 | *.out 15 | 16 | # Files generated during the build 17 | static/*.html 18 | *.json 19 | 20 | vendor/ 21 | *.log 22 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | language: go 3 | go: 1.15 4 | services: 5 | - docker 6 | 7 | install: 8 | - sudo apt-get install -y pandoc 9 | 10 | after_success: 11 | - bash <(curl -s https://codecov.io/bash) 12 | 13 | deploy: 14 | - provider: script 15 | script: make login && make push 16 | on: 17 | branch: master 18 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.17.6 2 | 3 | ENV SRC_DIR=/go/src/github.com/davemachado/public-api 4 | 5 | ADD . $SRC_DIR 6 | WORKDIR $SRC_DIR 7 | 8 | RUN CGO_ENABLED=0 GOOS=linux go build 9 | 10 | FROM scratch 11 | COPY --from=0 /go/src/github.com/davemachado/public-api/public-api /public-api 12 | COPY --from=0 /go/src/github.com/davemachado/public-api/static /static 13 | EXPOSE 8080 14 | ENTRYPOINT ["/public-api"] 15 | 16 | -------------------------------------------------------------------------------- /Gopkg.lock: -------------------------------------------------------------------------------- 1 | # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. 2 | 3 | 4 | [[projects]] 5 | name = "github.com/didip/tollbooth" 6 | packages = [ 7 | ".", 8 | "errors", 9 | "libstring", 10 | "limiter" 11 | ] 12 | revision = "c95eaa3ddc98f635a91e218b48727fb2e06613ea" 13 | version = "v4.0.0" 14 | 15 | [[projects]] 16 | branch = "master" 17 | name = "github.com/didip/tollbooth_negroni" 18 | packages = ["."] 19 | revision = "a4e3efc33255cee9eb045bac7016d1c2cdb63e67" 20 | 21 | [[projects]] 22 | branch = "master" 23 | name = "github.com/gorilla/schema" 24 | packages = ["."] 25 | revision = "afe77393c53b66afe9212810d9b2013859d04ae6" 26 | 27 | [[projects]] 28 | name = "github.com/patrickmn/go-cache" 29 | packages = ["."] 30 | revision = "a3647f8e31d79543b2d0f0ae2fe5c379d72cedc0" 31 | version = "v2.1.0" 32 | 33 | [[projects]] 34 | name = "github.com/urfave/negroni" 35 | packages = ["."] 36 | revision = "5dbbc83f748fc3ad38585842b0aedab546d0ea1e" 37 | version = "v0.3.0" 38 | 39 | [[projects]] 40 | branch = "master" 41 | name = "golang.org/x/net" 42 | packages = ["context"] 43 | revision = "f5dfe339be1d06f81b22525fe34671ee7d2c8904" 44 | 45 | [[projects]] 46 | branch = "master" 47 | name = "golang.org/x/time" 48 | packages = ["rate"] 49 | revision = "6dc17368e09b0e8634d71cac8168d853e869a0c7" 50 | 51 | [solve-meta] 52 | analyzer-name = "dep" 53 | analyzer-version = 1 54 | inputs-digest = "2379ac2e78f6f03cb87c2fac0f7eae6b2bb0362c980b4b3027d2b51e53b08567" 55 | solver-name = "gps-cdcl" 56 | solver-version = 1 57 | -------------------------------------------------------------------------------- /Gopkg.toml: -------------------------------------------------------------------------------- 1 | # Gopkg.toml example 2 | # 3 | # Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md 4 | # for detailed Gopkg.toml documentation. 5 | # 6 | # required = ["github.com/user/thing/cmd/thing"] 7 | # ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] 8 | # 9 | # [[constraint]] 10 | # name = "github.com/user/project" 11 | # version = "1.0.0" 12 | # 13 | # [[constraint]] 14 | # name = "github.com/user/project2" 15 | # branch = "dev" 16 | # source = "github.com/myfork/project2" 17 | # 18 | # [[override]] 19 | # name = "github.com/x/y" 20 | # version = "2.4.0" 21 | # 22 | # [prune] 23 | # non-go = false 24 | # go-tests = true 25 | # unused-packages = true 26 | 27 | 28 | [[constraint]] 29 | name = "github.com/didip/tollbooth" 30 | version = "4.0.0" 31 | 32 | [[constraint]] 33 | branch = "master" 34 | name = "github.com/didip/tollbooth_negroni" 35 | 36 | [[constraint]] 37 | branch = "master" 38 | name = "github.com/gorilla/schema" 39 | 40 | [[constraint]] 41 | name = "github.com/urfave/negroni" 42 | version = "0.3.0" 43 | 44 | [prune] 45 | go-tests = true 46 | unused-packages = true 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2018 Dave Machado 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | NAME := davemachado/public-api 2 | TAG := $$(git log -1 --pretty=%h) 3 | IMG := ${NAME}:${TAG} 4 | LATEST := ${NAME}:latest 5 | 6 | all: build 7 | 8 | dep: 9 | @go get -v ./... 10 | 11 | test: 12 | @go test -race -coverprofile=coverage.txt -covermode=atomic -v ./... 13 | 14 | build: dep test html 15 | @docker build -t ${IMG} . 16 | @docker tag ${IMG} ${LATEST} 17 | 18 | data: html 19 | @curl -o /tmp/public-apis.md https://raw.githubusercontent.com/public-apis/public-apis/master/README.md 20 | @./md2json /tmp/public-apis.md > entries.json 21 | @rm /tmp/public-apis.md 22 | 23 | html: 24 | mkdir -p static 25 | pandoc --from markdown_github --to html --standalone README.md > static/index.html 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Public API for Public APIs 2 | 3 | [![Build Status](https://travis-ci.org/davemachado/public-api.svg?branch=master)](https://travis-ci.org/davemachado/public-api) 4 | [![Go Report Card](https://goreportcard.com/badge/github.com/davemachado/public-api)](https://goreportcard.com/report/github.com/davemachado/public-api) 5 | 6 | Welcome to the official public API for the [public-apis](https://github.com/public-apis/public-apis) project! 7 | 8 | This service supports [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) and requires no authentication to use. All responses are sent over HTTPS as well. 9 | 10 | If you would like to leave feedback or request a feature, please [open an issue](https://github.com/davemachado/public-api/issues). If you would like to contribute, feel free to [open a pull request](https://github.com/davemachado/public-api/pulls). 11 | 12 | ## Github Project 13 | https://github.com/davemachado/public-api 14 | 15 | ## Base URL 16 | https://api.publicapis.org/ 17 | 18 | --- 19 | 20 | # Services 21 | ## **GET** /entries 22 | 23 | *List all entries currently cataloged in the project* 24 | 25 | ### Parameters 26 | Parameter | Type | Data Type | Description | Required 27 | | --- | --- | --- | --- | --- | 28 | | title | query | string | name of entry (matches via substring - i.e. "at" would return "cat" and "atlas") | No | 29 | | description | query | string | description of entry (matches via substring) | No | 30 | | auth | query | string | auth type of entry (can only be values matching in project or null) | No | 31 | | https | query | bool | return entries that support HTTPS or not | No | 32 | | cors | query | string | CORS support for entry ("yes", "no", or "unknown") | No | 33 | | category | query | string | return entries of a specific category | No | 34 | 35 | > For categories like "Science & Math" which have a space and an ampersand, the query is simply the first word. Using "Science & Math" as an example, the correct query would be `category=science` 36 | 37 | ## **GET** /random 38 | 39 | *List a single entry selected at random* 40 | 41 | ### Parameters 42 | Parameter | Type | Data Type | Description | Required 43 | | --- | --- | --- | --- | --- | 44 | | title | query | string | name of entry (matches via substring - i.e. "at" would return "cat" and "atlas") | No | 45 | | description | query | string | description of entry (matches via substring) | No | 46 | | auth | query | string | auth type of entry (can only be values matching in project or null) | No | 47 | | https | query | bool | return entries that support HTTPS or not | No | 48 | | cors | query | string | CORS support for entry ("yes", "no", or "unknown") | No | 49 | | category | query | string | return entries of a specific category | No | 50 | 51 | ## **GET** /categories 52 | 53 | *List all categories* 54 | 55 | ### Parameters 56 | None 57 | 58 | ## **GET** /health 59 | 60 | *Check health of the running service* 61 | 62 | ### Parameters 63 | None 64 | 65 | --- 66 | [![DigitalOcean](https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/PoweredByDO/DO_Powered_by_Badge_blue.png)](https://www.digitalocean.com/) 67 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/davemachado/public-api 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/didip/tollbooth v4.0.0+incompatible 7 | github.com/didip/tollbooth_negroni v0.0.0-20170928042109-a4e3efc33255 8 | github.com/gorilla/schema v0.0.0-20171228183507-afe77393c53b 9 | github.com/patrickmn/go-cache v2.1.0+incompatible 10 | github.com/urfave/negroni v0.3.0 11 | golang.org/x/net v0.0.0-20180208041118-f5dfe339be1d 12 | golang.org/x/time v0.0.0-20170927054726-6dc17368e09b 13 | ) 14 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/didip/tollbooth v4.0.0+incompatible h1:ayQZYuF5QOxx3NdYRNuRVFLv9/2b64JtSUlewb+0TMo= 2 | github.com/didip/tollbooth v4.0.0+incompatible/go.mod h1:A9b0665CE6l1KmzpDws2++elm/CsuWBMa5Jv4WY0PEY= 3 | github.com/didip/tollbooth_negroni v0.0.0-20170928042109-a4e3efc33255 h1:DBDcY1PjsPn8muU2Ne7rQvZuLvW289Jd289uYKfMP/c= 4 | github.com/didip/tollbooth_negroni v0.0.0-20170928042109-a4e3efc33255/go.mod h1:6k7eE4KNeLUhxn7kL8NrvJfj6BJ5zCq5GV3P29S7+pQ= 5 | github.com/gorilla/schema v0.0.0-20171228183507-afe77393c53b h1:H26T5o2nhx7uIvhRLWRspZIUGBh7x4y2L/6aYo8GxqU= 6 | github.com/gorilla/schema v0.0.0-20171228183507-afe77393c53b/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU= 7 | github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= 8 | github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= 9 | github.com/urfave/negroni v0.3.0 h1:PaXOb61mWeZJxc1Ji2xJjpVg9QfPo0rrB+lHyBxGNSU= 10 | github.com/urfave/negroni v0.3.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4= 11 | golang.org/x/net v0.0.0-20180208041118-f5dfe339be1d h1:lnO2rP1Eit1fCAJKjYJlnArsHluPBxcs2BA2dQrL224= 12 | golang.org/x/net v0.0.0-20180208041118-f5dfe339be1d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 13 | golang.org/x/time v0.0.0-20170927054726-6dc17368e09b h1:3X+R0qq1+64izd8es+EttB6qcY+JDlVmAhpRXl7gpzU= 14 | golang.org/x/time v0.0.0-20170927054726-6dc17368e09b/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 15 | -------------------------------------------------------------------------------- /handlers.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "io" 6 | "math/rand" 7 | "net/http" 8 | ) 9 | 10 | type ( 11 | // SearchRequest describes an incoming search request. 12 | SearchRequest struct { 13 | Title string `schema:"title"` 14 | Description string `schema:"description"` 15 | Auth string `schema:"auth"` 16 | HTTPS string `schema:"https"` 17 | Cors string `schema:"cors"` 18 | Category string `schema:"category"` 19 | } 20 | // Entries contains an array of API entries, and a count representing the length of that array. 21 | Entries struct { 22 | Count int `json:"count"` 23 | Entries []Entry `json:"entries"` 24 | } 25 | // Categories contains a set of category strings, and a count representing the length of that array. 26 | Categories struct { 27 | Count int `json:"count"` 28 | Categories []string `json:"categories"` 29 | } 30 | // Entry describes a single API reference. 31 | Entry struct { 32 | API string `json:"API"` 33 | Description string `json:"Description"` 34 | Auth string `json:"Auth"` 35 | HTTPS bool `json:"HTTPS"` 36 | Cors string `json:"Cors"` 37 | Link string `json:"Link"` 38 | Category string `json:"Category"` 39 | } 40 | ) 41 | 42 | // getEntriesHandler returns an Entries object with the matching entries filtered 43 | // by the search request 44 | func getEntriesHandler() http.Handler { 45 | return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 46 | if req.Method != http.MethodGet { 47 | w.WriteHeader(http.StatusMethodNotAllowed) 48 | return 49 | } 50 | results, err := processSearchRequestToMatchingEntries(req) 51 | if err != nil { 52 | http.Error(w, err.Error(), http.StatusBadRequest) 53 | return 54 | } 55 | w.Header().Set("Content-Type", "application/json") 56 | w.Header().Set("Access-Control-Allow-Origin", "*") 57 | err = json.NewEncoder(w).Encode(Entries{ 58 | Count: len(results), 59 | Entries: results, 60 | }) 61 | if err != nil { 62 | http.Error(w, "server failed to encode response object: "+err.Error(), http.StatusInternalServerError) 63 | return 64 | } 65 | }) 66 | } 67 | 68 | // getCategoriesHandler returns a string slice object with all unique categories 69 | func getCategoriesHandler() http.Handler { 70 | return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 71 | if req.Method != http.MethodGet { 72 | w.WriteHeader(http.StatusMethodNotAllowed) 73 | return 74 | } 75 | w.Header().Set("Content-Type", "application/json") 76 | w.Header().Set("Access-Control-Allow-Origin", "*") 77 | err := json.NewEncoder(w).Encode(Categories{ 78 | Count: len(categories), 79 | Categories: categories, 80 | }) 81 | if err != nil { 82 | http.Error(w, "server failed to encode response object: "+err.Error(), http.StatusInternalServerError) 83 | return 84 | } 85 | }) 86 | } 87 | 88 | // getRandomHandler returns an Entries object containing a random element from the Entries slice 89 | func getRandomHandler() http.Handler { 90 | return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 91 | if req.Method != http.MethodGet { 92 | w.WriteHeader(http.StatusMethodNotAllowed) 93 | return 94 | } 95 | results, err := processSearchRequestToMatchingEntries(req) 96 | if err != nil { 97 | http.Error(w, err.Error(), http.StatusBadRequest) 98 | return 99 | } 100 | w.Header().Set("Content-Type", "application/json") 101 | w.Header().Set("Access-Control-Allow-Origin", "*") 102 | err = json.NewEncoder(w).Encode(Entries{ 103 | Count: 1, 104 | Entries: []Entry{results[rand.Intn(len(results))]}, 105 | }) 106 | if err != nil { 107 | http.Error(w, "server failed to encode response object: "+err.Error(), http.StatusInternalServerError) 108 | return 109 | } 110 | }) 111 | } 112 | 113 | // healthCheckHandler returns a simple indication on whether or not the core http service is running 114 | func healthCheckHandler() http.Handler { 115 | return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 116 | w.WriteHeader(http.StatusOK) 117 | w.Header().Set("Content-Type", "application/json") 118 | w.Header().Set("Access-Control-Allow-Origin", "*") 119 | io.WriteString(w, `{"alive": true}`) 120 | }) 121 | } 122 | -------------------------------------------------------------------------------- /handlers_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "net/http" 7 | "net/http/httptest" 8 | "reflect" 9 | "testing" 10 | ) 11 | 12 | func mockEntry() []Entry { 13 | return []Entry{ 14 | { 15 | API: "title", 16 | Description: "description", 17 | Auth: "apiKey", 18 | HTTPS: false, 19 | Cors: "Cors", 20 | Link: "link", 21 | Category: "category", 22 | }, 23 | } 24 | } 25 | 26 | func assertResponseValid(t *testing.T, body *bytes.Buffer, expected []Entry) { 27 | var resp Entries 28 | if err := json.NewDecoder(body).Decode(&resp); err != nil { 29 | t.Fatal(err) 30 | } 31 | if !reflect.DeepEqual(resp.Entries, expected) { 32 | t.Fatalf("handler returned wrong entry: got %v want %v", 33 | resp.Entries, expected) 34 | } 35 | 36 | } 37 | 38 | func TestHealthCheckHandler(t *testing.T) { 39 | req, err := http.NewRequest("GET", "/health", nil) 40 | if err != nil { 41 | t.Fatal(err) 42 | } 43 | rr := httptest.NewRecorder() 44 | handler := healthCheckHandler() 45 | handler.ServeHTTP(rr, req) 46 | if status := rr.Code; status != http.StatusOK { 47 | t.Errorf("handler returned wrong status code: got %v want %v", 48 | status, http.StatusOK) 49 | } 50 | expected := `{"alive": true}` 51 | if rr.Body.String() != expected { 52 | t.Errorf("handler returned unexpected body: got %v want %v", 53 | rr.Body.String(), expected) 54 | } 55 | } 56 | 57 | func TestGetCategoriesHandler(t *testing.T) { 58 | req, err := http.NewRequest("GET", "/categories", nil) 59 | if err != nil { 60 | t.Fatal(err) 61 | } 62 | testCases := []struct { 63 | categories []string 64 | expectedBody string 65 | }{ 66 | {[]string{}, "{\"count\":0,\"categories\":[]}\n"}, 67 | {[]string{"cat1"}, "{\"count\":1,\"categories\":[\"cat1\"]}\n"}, 68 | {[]string{"cat1", "cat2", "cat3"}, "{\"count\":3,\"categories\":[\"cat1\",\"cat2\",\"cat3\"]}\n"}, 69 | } 70 | for _, tc := range testCases { 71 | categories = tc.categories 72 | rr := httptest.NewRecorder() 73 | handler := getCategoriesHandler() 74 | handler.ServeHTTP(rr, req) 75 | if status := rr.Code; status != http.StatusOK { 76 | t.Errorf("handler returned wrong status code: got %v want %v", 77 | status, http.StatusOK) 78 | } 79 | if rr.Body.String() != tc.expectedBody { 80 | t.Errorf("handler returned wrong body: got %q want %q", rr.Body, tc.expectedBody) 81 | } 82 | } 83 | } 84 | 85 | func TestGetRandomHandler(t *testing.T) { 86 | req, err := http.NewRequest("GET", "/random", nil) 87 | if err != nil { 88 | t.Fatal(err) 89 | } 90 | apiList.Entries = mockEntry() 91 | rr := httptest.NewRecorder() 92 | handler := getRandomHandler() 93 | handler.ServeHTTP(rr, req) 94 | if status := rr.Code; status != http.StatusOK { 95 | t.Errorf("handler returned wrong status code: got %v want %v", 96 | status, http.StatusOK) 97 | } 98 | assertResponseValid(t, rr.Body, apiList.Entries) 99 | } 100 | 101 | func TestGetEntriesHandler(t *testing.T) { 102 | req, err := http.NewRequest("GET", "/api", nil) 103 | if err != nil { 104 | t.Fatal(err) 105 | } 106 | apiList.Entries = mockEntry() 107 | rr := httptest.NewRecorder() 108 | handler := getEntriesHandler() 109 | handler.ServeHTTP(rr, req) 110 | if status := rr.Code; status != http.StatusOK { 111 | t.Errorf("handler returned wrong status code: got %v want %v", 112 | status, http.StatusOK) 113 | } 114 | assertResponseValid(t, rr.Body, apiList.Entries) 115 | } 116 | 117 | func TestGetEntriesWithBadMethod(t *testing.T) { 118 | req, err := http.NewRequest("POST", "/api", nil) 119 | if err != nil { 120 | t.Fatal(err) 121 | } 122 | rr := httptest.NewRecorder() 123 | handler := getEntriesHandler() 124 | handler.ServeHTTP(rr, req) 125 | if status := rr.Code; status != http.StatusMethodNotAllowed { 126 | t.Errorf("handler returned wrong status code: got %v want %v", 127 | status, http.StatusMethodNotAllowed) 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /logger.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "log" 6 | "net/http" 7 | "os" 8 | "time" 9 | ) 10 | 11 | type ( 12 | // Options is a struct for specifying configuration parameters for the Logger middleware. 13 | Options struct { 14 | // Prefix is the outputted keyword in front of the log message. Logger automatically wraps the prefix in square brackets (ie. [myApp] ) unless the `DisableAutoBrackets` is set to true. A blank value will not have brackets added. Default is blank (with no brackets). 15 | Prefix string 16 | // DisableAutoBrackets if set to true, will remove the prefix and square brackets. Default is false. 17 | DisableAutoBrackets bool 18 | // RemoteAddressHeaders is a list of header keys that Logger will look at to determine the proper remote address. Useful when using a proxy like Nginx: `[]string{"X-Forwarded-Proto"}`. Default is an empty slice, and thus will use `reqeust.RemoteAddr`. 19 | RemoteAddressHeaders []string 20 | // Out is the destination to which the logged data will be written too. Default is `os.Stdout`. 21 | Out io.Writer 22 | // OutputFlags defines the logging properties. See http://golang.org/pkg/log/#pkg-constants. To disable all flags, set this to `-1`. Defaults to log.LstdFlags (2009/01/23 01:23:23). 23 | OutputFlags int 24 | // IgnoredRequestURIs is a list of path values we do not want logged out. Exact match only! 25 | IgnoredRequestURIs []string 26 | } 27 | // Logger is a HTTP middleware handler that logs a request. Outputted information includes status, method, URL, remote address, size, and the time it took to process the request. 28 | Logger struct { 29 | *log.Logger 30 | opt Options 31 | } 32 | // customResponseWriter is a wrapper around golang's standard ResponseWriter to include a status code and response size 33 | customResponseWriter struct { 34 | http.ResponseWriter 35 | status int 36 | size int 37 | } 38 | ) 39 | 40 | // NewLogger returns a reference to a configured instance of Logger 41 | func NewLogger(opts ...Options) *Logger { 42 | var o Options 43 | if len(opts) == 0 { 44 | o = Options{} 45 | } else { 46 | o = opts[0] 47 | } 48 | // Determine prefix. 49 | prefix := o.Prefix 50 | if len(prefix) > 0 && o.DisableAutoBrackets == false { 51 | prefix = "[" + prefix + "] " 52 | } 53 | // Determine output writer. 54 | var output io.Writer 55 | if o.Out != nil { 56 | output = o.Out 57 | } else { 58 | // Default is stdout. 59 | output = os.Stdout 60 | } 61 | // Determine output flags. 62 | flags := log.LstdFlags 63 | if o.OutputFlags == -1 { 64 | flags = 0 65 | } else if o.OutputFlags != 0 { 66 | flags = o.OutputFlags 67 | } 68 | return &Logger{ 69 | Logger: log.New(output, prefix, flags), 70 | opt: o, 71 | } 72 | } 73 | 74 | func (l *Logger) logFunc(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) { 75 | start := time.Now() 76 | crw := newCustomResponseWriter(rw) 77 | next(crw, r) 78 | end := time.Now() 79 | for _, ignoredURI := range l.opt.IgnoredRequestURIs { 80 | if ignoredURI == r.RequestURI { 81 | return 82 | } 83 | } 84 | addr := r.RemoteAddr 85 | headersToCheck := []string{"X-Real-Ip", "X-Forwarded-For"} 86 | for _, headerKey := range headersToCheck { 87 | if val := r.Header.Get(headerKey); len(val) > 0 { 88 | addr = val 89 | break 90 | } 91 | } 92 | l.Printf("| %3d | %13v | %50s | %8d | %5s %s\n", crw.status, end.Sub(start), addr, crw.size, r.Method, r.RequestURI) 93 | } 94 | 95 | func (c *customResponseWriter) WriteHeader(status int) { 96 | c.status = status 97 | c.ResponseWriter.WriteHeader(status) 98 | } 99 | 100 | func (c *customResponseWriter) Write(b []byte) (int, error) { 101 | size, err := c.ResponseWriter.Write(b) 102 | c.size += size 103 | return size, err 104 | } 105 | 106 | func newCustomResponseWriter(w http.ResponseWriter) *customResponseWriter { 107 | // When WriteHeader is not called, it's safe to assume the status will be 200. 108 | return &customResponseWriter{ 109 | ResponseWriter: w, 110 | status: 200, 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /logger_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "reflect" 7 | "testing" 8 | ) 9 | 10 | var ( 11 | myHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 12 | w.Write([]byte("bar")) 13 | }) 14 | myHandlerWithError = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 15 | http.Error(w, http.StatusText(http.StatusBadGateway), http.StatusBadGateway) 16 | }) 17 | ) 18 | 19 | func expect(t *testing.T, a interface{}, b interface{}) { 20 | if a != b { 21 | t.Errorf("Expected [%v] (type %v) - Got [%v] (type %v)", b, reflect.TypeOf(b), a, reflect.TypeOf(a)) 22 | } 23 | } 24 | 25 | func TestNoConfig(t *testing.T) { 26 | res := httptest.NewRecorder() 27 | req, _ := http.NewRequest("GET", "/should/be/stdout/", nil) 28 | req.RemoteAddr = "111.222.333.444" 29 | myHandler.ServeHTTP(res, req) 30 | expect(t, res.Code, http.StatusOK) 31 | expect(t, res.Body.String(), "bar") 32 | } 33 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "log" 6 | "net/http" 7 | "os" 8 | "strconv" 9 | 10 | "github.com/didip/tollbooth" 11 | "github.com/didip/tollbooth_negroni" 12 | "github.com/urfave/negroni" 13 | ) 14 | 15 | var apiList Entries 16 | var categories []string 17 | 18 | func main() { 19 | jsonFile := os.Getenv("JSONFILE") 20 | if jsonFile == "" { 21 | jsonFile = "/entries.json" 22 | } 23 | getList(jsonFile) 24 | categories = parseCategories(apiList.Entries) 25 | port := os.Getenv("PORT") 26 | if port == "" { 27 | port = "8080" 28 | } 29 | rate := os.Getenv("RATE") 30 | if rate == "" { 31 | rate = "10" 32 | } 33 | i, err := strconv.Atoi(rate) 34 | if err != nil { 35 | panic(err) 36 | } 37 | limiter := tollbooth.NewLimiter(float64(i), nil) 38 | 39 | filename := os.Getenv("LOGFILE") 40 | if filename == "" { 41 | filename = "/tmp/public-api.log" 42 | } 43 | // If the file does not exist, create it. Otherwise, append to the file. 44 | f, err := os.OpenFile(filename, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) 45 | if err != nil { 46 | log.Fatal(err) 47 | } 48 | if err != nil { 49 | panic(err) 50 | } 51 | logger := NewLogger(Options{ 52 | Out: io.MultiWriter(f, os.Stdout), 53 | }) 54 | 55 | mux := http.NewServeMux() 56 | mux.Handle("/", http.FileServer(http.Dir("./static"))) 57 | mux.Handle("/entries", negroni.New( 58 | tollbooth_negroni.LimitHandler(limiter), 59 | negroni.Wrap(getEntriesHandler()), 60 | )) 61 | mux.Handle("/categories", negroni.New( 62 | tollbooth_negroni.LimitHandler(limiter), 63 | negroni.Wrap(getCategoriesHandler()), 64 | )) 65 | mux.Handle("/random", negroni.New( 66 | tollbooth_negroni.LimitHandler(limiter), 67 | negroni.Wrap(getRandomHandler()), 68 | )) 69 | mux.Handle("/health", negroni.New( 70 | tollbooth_negroni.LimitHandler(limiter), 71 | negroni.Wrap(healthCheckHandler()), 72 | )) 73 | 74 | n := negroni.New(negroni.HandlerFunc(logger.logFunc), negroni.HandlerFunc(encodeURL)) 75 | recovery := negroni.NewRecovery() 76 | recovery.PrintStack = false 77 | n.Use(recovery) 78 | n.UseHandler(mux) 79 | 80 | log.Println("logging requests in " + filename) 81 | log.Printf("listening on port %s\n", port) 82 | log.Fatal(http.ListenAndServe(":"+port, n)) 83 | } 84 | -------------------------------------------------------------------------------- /md2json: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import json 4 | import re 5 | import sys 6 | 7 | def markdown_to_json(filename, anchor): 8 | """Convert a Markdown file into a JSON string""" 9 | category = "" 10 | entries = [] 11 | link_re = re.compile('\[(.+)\]\((http.*)\)') 12 | with open(filename) as fp: 13 | lines = (line.rstrip() for line in fp) 14 | lines = list(line for line in lines if line and 15 | line.startswith(anchor) or line.startswith('| ')) 16 | for line in lines: 17 | if line.startswith(anchor): 18 | category = line.split(anchor)[1].strip() 19 | continue 20 | chunks = [x.strip() for x in line.split('|')[1:-1]] 21 | raw_title = chunks[0] 22 | title_re_match = link_re.match(raw_title) 23 | if not title_re_match: 24 | print("could not match {} to Link RegEx".format(raw_title)) 25 | sys.exit(1) 26 | title = title_re_match.group(1) 27 | link = title_re_match.group(2) 28 | entry = { 29 | 'API': title, 30 | 'Description': chunks[1], 31 | 'Auth': None if chunks[2].upper() == 'NO' else chunks[2].strip('`'), 32 | 'HTTPS': True if chunks[3].upper() == 'YES' else False, 33 | 'CORS': chunks[4].strip('`').lower(), 34 | 'Link': link, 35 | 'Category': category, 36 | } 37 | entries.append(entry) 38 | final = { 39 | 'count': len(entries), 40 | 'entries': entries, 41 | } 42 | return json.dumps(final) 43 | 44 | 45 | def main(): 46 | num_args = len(sys.argv) 47 | if num_args < 2: 48 | print("No .md file passed") 49 | sys.exit(1) 50 | if num_args < 3: 51 | anchor = '###' 52 | else: 53 | anchor = sys.argv[2] 54 | print(markdown_to_json(sys.argv[1], anchor)) 55 | 56 | 57 | if __name__ == "__main__": 58 | main() 59 | -------------------------------------------------------------------------------- /rotate-log: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [ "$#" -ne 2 ]; then 4 | echo "error: $0 " 5 | exit 1 6 | fi 7 | 8 | case "$2" in 9 | */) 10 | DIR=$2 11 | ;; 12 | *) 13 | DIR="$2/" 14 | ;; 15 | esac 16 | 17 | source=$1 18 | DATE=`date +%Y-%m-%d_%H:%M` 19 | target="$DIR$DATE.log" 20 | 21 | cp $source $target 22 | > $source 23 | -------------------------------------------------------------------------------- /util.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | "net/url" 8 | "os" 9 | "strconv" 10 | "strings" 11 | 12 | "github.com/gorilla/schema" 13 | ) 14 | 15 | func encodeURL(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) { 16 | encoded := strings.Replace(r.URL.String(), "%20&%20", "%20%26%20", -1) 17 | r.URL, _ = url.Parse(encoded) 18 | next(rw, r) 19 | } 20 | 21 | // getList initializes an Entries struct filled from the public-apis project 22 | func getList(jsonFile string) { 23 | file, err := os.OpenFile(jsonFile, os.O_RDONLY, 0644) 24 | if err != nil { 25 | panic("failed to open file: " + err.Error()) 26 | } 27 | 28 | err = json.NewDecoder(file).Decode(&apiList) 29 | if err != nil { 30 | panic("failed to decode JSON from file: " + err.Error()) 31 | } 32 | file.Close() 33 | } 34 | 35 | // getCategories initializes a string slice containing 36 | // all unique categories from a given slice of Entries 37 | func parseCategories(entries []Entry) []string { 38 | var cats []string 39 | set := make(map[string]struct{}) 40 | for _, entry := range entries { 41 | if _, exists := set[entry.Category]; !exists { 42 | cats = append(cats, entry.Category) 43 | set[entry.Category] = struct{}{} 44 | } 45 | } 46 | return cats 47 | } 48 | 49 | // checkEntryMatches checks if the given entry matches the given request's parameters. 50 | // it returns true if the entry matches, and returns false otherwise. 51 | func checkEntryMatches(entry Entry, request *SearchRequest) bool { 52 | if strings.Contains(strings.ToLower(entry.API), strings.ToLower(request.Title)) && 53 | strings.Contains(strings.ToLower(entry.Description), strings.ToLower(request.Description)) && 54 | ((strings.ToLower(request.Auth) == "null" && entry.Auth == "") || strings.Contains(strings.ToLower(entry.Auth), strings.ToLower(request.Auth))) && 55 | strings.Contains(strings.ToLower(entry.Cors), strings.ToLower(request.Cors)) && 56 | strings.Contains(strings.ToLower(entry.Category), strings.ToLower(request.Category)) { 57 | if request.HTTPS == "" { 58 | return true 59 | } 60 | if value, err := strconv.ParseBool(request.HTTPS); err == nil { 61 | if entry.HTTPS == value { 62 | return true 63 | } 64 | } 65 | } 66 | return false 67 | } 68 | 69 | // processSearchRequestToMatchingEntries decodes the request body into a SearchRequest struct that can 70 | // be processed in a call to checkEntryMatches to return all matching entries. 71 | func processSearchRequestToMatchingEntries(req *http.Request) ([]Entry, error) { 72 | searchReq := new(SearchRequest) 73 | // Decode incoming search request off the query parameters map. 74 | if err := schema.NewDecoder().Decode(searchReq, req.URL.Query()); err != nil { 75 | return nil, fmt.Errorf("server failed to parse request: %v", err) 76 | } 77 | var results []Entry 78 | for _, e := range apiList.Entries { 79 | if checkEntryMatches(e, searchReq) { 80 | results = append(results, e) 81 | } 82 | } 83 | return results, nil 84 | } 85 | -------------------------------------------------------------------------------- /util_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "net/url" 7 | "reflect" 8 | "testing" 9 | ) 10 | 11 | func TestEncodeURL(t *testing.T) { 12 | testCases := []struct { 13 | actual string 14 | expected string 15 | }{ 16 | {"/entries?category=Science%20%26%20Math", "/entries?category=Science%20%26%20Math"}, 17 | {"/entries?category=Science & Math", "/entries?category=Science & Math"}, 18 | {"/entries?category=Science%20Math", "/entries?category=Science%20Math"}, 19 | } 20 | 21 | for _, tc := range testCases { 22 | req, _ := http.NewRequest("GET", tc.actual, nil) 23 | rr := httptest.NewRecorder() 24 | encodeURL(rr, req, 25 | http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {})) 26 | if req.URL.String() != tc.expected { 27 | t.Errorf("incorrect encoding: expected %q, got %q", tc.expected, tc.actual) 28 | } 29 | } 30 | } 31 | 32 | func TestGetCategories(t *testing.T) { 33 | actual := parseCategories([]Entry{ 34 | Entry{Category: "A"}, 35 | Entry{Category: "B"}, 36 | Entry{Category: "B"}, 37 | Entry{Category: "C"}, 38 | Entry{Category: "D"}, 39 | }) 40 | expected := []string{"A", "B", "C", "D"} 41 | if len(actual) != len(expected) { 42 | t.Fatalf("bad parsing: expected %v, got %v", expected, actual) 43 | } 44 | for i := 0; i < len(expected); i++ { 45 | if actual[i] != expected[i] { 46 | t.Errorf("bad element: expected %q, got %q", actual[i], expected[i]) 47 | } 48 | } 49 | } 50 | 51 | func TestCheckEntryMatches(t *testing.T) { 52 | entry := Entry{ 53 | API: "examplesAsAService", 54 | Description: "provide classic examples", 55 | Auth: "apiKey", 56 | HTTPS: true, 57 | Cors: "Unknown", 58 | Link: "http://www.example.com", 59 | Category: "Development", 60 | } 61 | entryEmptyAuth := Entry{ 62 | API: "examplesAsAService", 63 | Description: "provide classic examples", 64 | Auth: "", 65 | HTTPS: true, 66 | Cors: "Unknown", 67 | Link: "http://www.example.com", 68 | Category: "Development", 69 | } 70 | 71 | testCases := []struct { 72 | name string 73 | entry Entry 74 | search *SearchRequest 75 | shouldPass bool 76 | }{ 77 | {"Full search", entry, &SearchRequest{}, true}, 78 | {"Desc valid full", entry, &SearchRequest{Description: "provide classic examples"}, true}, 79 | {"Desc valid match", entry, &SearchRequest{Description: "provide class"}, true}, 80 | {"Desc invalid", entry, &SearchRequest{Description: "this will not match"}, false}, 81 | {"Auth valid full", entry, &SearchRequest{Auth: "apiKey"}, true}, 82 | {"Auth valid match", entry, &SearchRequest{Auth: "apiK"}, true}, 83 | {"Auth empty", entry, &SearchRequest{Auth: ""}, true}, 84 | {"Auth empty entry", entryEmptyAuth, &SearchRequest{Auth: ""}, true}, 85 | {"Auth null", entry, &SearchRequest{Auth: "null"}, false}, 86 | {"Auth null empty entry", entryEmptyAuth, &SearchRequest{Auth: "null"}, true}, 87 | {"Auth invalid", entry, &SearchRequest{Auth: "foo"}, false}, 88 | {"HTTPS true", entry, &SearchRequest{HTTPS: "1"}, true}, 89 | {"HTTPS false", entry, &SearchRequest{HTTPS: "false"}, false}, 90 | {"CORS valid full", entry, &SearchRequest{Cors: "unknown"}, true}, 91 | {"CORS valid match", entry, &SearchRequest{Cors: "unk"}, true}, 92 | {"CORS invalid", entry, &SearchRequest{Cors: "bar"}, false}, 93 | } 94 | for _, tc := range testCases { 95 | t.Run(tc.name, func(t *testing.T) { 96 | if checkEntryMatches(tc.entry, tc.search) != tc.shouldPass { 97 | if tc.shouldPass { 98 | t.Errorf("was expecting to pass, but failed") 99 | } else { 100 | t.Errorf("was expecting to fail, but passed") 101 | } 102 | } 103 | }) 104 | } 105 | } 106 | 107 | func TestProcessSearchRequestToMatchingEntries(t *testing.T) { 108 | apiList.Entries = []Entry{ 109 | Entry{ 110 | API: "examplesAsAService", 111 | Description: "provide classic examples", 112 | Auth: "apiKey", 113 | HTTPS: true, 114 | Cors: "Unknown", 115 | Link: "http://www.example.com", 116 | Category: "Development", 117 | }, 118 | Entry{ 119 | API: "examplesAsAServiceToo", 120 | Description: "provide classic examples", 121 | Auth: "", 122 | HTTPS: true, 123 | Cors: "Yes", 124 | Link: "http://www.example.com", 125 | Category: "Development", 126 | }, 127 | } 128 | testCases := []struct { 129 | name string 130 | query string 131 | expected []Entry 132 | }{ 133 | {"null auth", "?auth=null", []Entry{apiList.Entries[1]}}, 134 | {"apiKey auth", "?auth=apiKey", []Entry{apiList.Entries[0]}}, 135 | {"multi-key query", "?auth=null&description=example", []Entry{apiList.Entries[1]}}, 136 | {"multi-key query full match", "?category=development&description=example", apiList.Entries}, 137 | {"fully-matching description", "?description=example", apiList.Entries}, 138 | {"unkwown cors", "?cors=unknown", []Entry{apiList.Entries[0]}}, 139 | {"yes cors", "?cors=yes", []Entry{apiList.Entries[1]}}, 140 | } 141 | 142 | for _, tc := range testCases { 143 | t.Run(tc.name, func(t *testing.T) { 144 | u, err := url.Parse(tc.query) 145 | if err != nil { 146 | t.Fatal(err) 147 | } 148 | req := &http.Request{URL: u} 149 | actual, err := processSearchRequestToMatchingEntries(req) 150 | if err != nil { 151 | t.Error(err) 152 | } 153 | if !reflect.DeepEqual(actual, tc.expected) { 154 | t.Errorf("unexpected matched entries:\nreceived %+v\nexpected %+v\n", actual, tc.expected) 155 | } 156 | }) 157 | } 158 | 159 | } 160 | --------------------------------------------------------------------------------