diff --git a/README.md b/README.md index d9c9edb..a9a2b74 100644 --- a/README.md +++ b/README.md @@ -8,10 +8,10 @@ [![GoDoc](https://godoc.org/github.com/kamilsk/form-api?status.svg)](https://godoc.org/github.com/kamilsk/form-api) [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) -## Usage +## Quick start ```bash -$ make deps up demo && make status +$ make deps up && make demo && make status Name Command State Ports ------------------------------------------------------------------------------------------ @@ -54,14 +54,31 @@ Flags: ## Installation -### Requirements +### Brew -- Docker 17.09.0-ce or above -- Docker Compose 1.16.1 or above -- Go 1.9.2 or above -- GNU Make 3.81 or above +```bash +$ brew install kamilsk/tap/form-api +``` + +### Binary + +```bash +$ export FAPI_V=1.0.4 # all available versions are on https://github.com/kamilsk/form-api/releases +$ export REQ_OS=Linux # macOS and Windows are also available +$ export REQ_ARCH=64bit # 32bit is also available +$ wget -q -O form-api.tar.gz \ + https://github.com/kamilsk/form-api/releases/download/${FAPI_V}/form-api_${FAPI_V}_${REQ_OS}-${REQ_ARCH}.tar.gz +$ tar xf form-api.tar.gz -C "${GOPATH}"/bin/ +$ rm form-api.tar.gz +``` -### From source +### Docker Hub + +```bash +$ docker pull kamilsk/form-api:latest +``` + +### From source code ```bash $ go get -d -u github.com/kamilsk/form-api @@ -77,6 +94,13 @@ $ egg bitbucket.org/kamilsk/form-api > [egg](https://github.com/kamilsk/egg) is an `extended go get`. +#### Requirements + +- Docker 17.09.0-ce or above +- Docker Compose 1.16.1 or above +- Go 1.9.2 or above +- GNU Make 3.81 or above + ## Feedback [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/kamilsk/form-api) @@ -90,9 +114,7 @@ $ egg bitbucket.org/kamilsk/form-api - [ ] v2: CRUD - [ ] v3: GUI - [ ] v4: API v2 - - [ ] v5: Scalability - - [ ] v6: Integrability - - [ ] v7: Performance and Redundancy - - [ ] v8: Complexity + - [ ] v5: Extensibility + - [ ] FormA, SaaS - tested on Go 1.9 - made with ❤️ by [OctoLab](https://www.octolab.org/) diff --git a/docs/action.js b/docs/action.js deleted file mode 100644 index bcff506..0000000 --- a/docs/action.js +++ /dev/null @@ -1,37 +0,0 @@ -(function ($) { - 'use strict'; - const location = new URL(window.location.href), - body = $('body'), - success = 'success', - failure = 'failure'; - $('.example form').each(function (i, node) { - const value = location.searchParams.get(node.id); - if (value) { - var msg, title; - switch (value) { - case success: - title = 'Form "' + node.title + '" was processed successfully!'; - msg = $('
'); - msg.append($('

').text(title)); - msg.append($('

Aww yeah, you did the right thing!

')); - break; - case failure: - title = 'Form "' + node.title + '" was processed unsuccessfully'; - msg = $('
'); - msg.append($('

').text(title)); - msg.append($('

Oops! But this also happens 😉

')); - break; - default: - return; - } - msg.append($('
')); - msg.append($('

As you can see it was very simple! 🤗

')); - msg.append( - $('

-
-
+
+

- - -

Motivation

@@ -94,24 +91,32 @@

Motivation

Open Source

-

...

+

We found some services like Wufoo and Paperclip are useful. + However, they are proprietary, closed and paid. Also, we want to have full control of our data. + With love for open source, we start to create something similar from scratch and share it with + our community.

-

Static Site Generators

-

...

+

Static Sites

+

We use static site generators like awesome Hugo + in our everyday work. We chose to get missing functionality from microservices like the + Form API. You can use it wherever you want - on simple landing pages or sites + based on Hugo or Gatsby, + or product from the StaticGen list.

-

Performance and Scalability

-

...

+

Performance

+

We focused on performance and simplicity because the service is self-hosted and we + do not want to spend much money, time and effort to host and maintain it. Therefore, we apply + efficient solutions, such as Go, + nginx and + PostgreSQL.

- - -

Live examples

@@ -192,29 +197,26 @@

Live examples

-
-

-<form lang="en" title="GitHub demo page"
+                    
+
+
<form lang="en" title="GitHub demo page"
       action="https://kamilsk.github.io/form-api/"
       method="post" enctype="application/x-www-form-urlencoded">
     <input name="name" type="text" title="Name"
            placeholder="Name..." maxlength="25" required="1"/>
     <input name="feedback" type="text" title="Feedback"
            placeholder="Your feedback..." maxlength="255" required="1"/>
-</form>
-                        
-

-<form lang="en" title="GitHub демо"
+</form>
+
<form lang="en" title="GitHub демо"
       action="https://kamilsk.github.io/form-api/"
       method="post" enctype="application/x-www-form-urlencoded">
     <input name="name" type="text" title="Имя"
            placeholder="Имя..." maxlength="25" required="1"/>
     <input name="feedback" type="text" title="Комментарий"
            placeholder="Ваш комментарий..." maxlength="255" required="1"/>
-</form>
-                        
+</form>
-
+
-
-

-<form lang="en" title="Email subscription"
+                    
+
+
<form lang="en" title="Email subscription"
       action="https://kamilsk.github.io/form-api/"
       method="post" enctype="application/x-www-form-urlencoded">
   <input name="email" type="email" title="Email"
          maxlength="64" required="1"/>
-</form>
-                        
+</form>
@@ -285,17 +286,122 @@

Live examples

rel="noopener nofollow"> Powered by DigitalOcean +


+ OctoLab
+
+
+

Up and running

+
+
+

from source code

+
+
$ go get -d -u github.com/kamilsk/form-api
+$ cd ${GOPATH}/src/github.com/kamilsk/form-api
 
+# test it
+$ make deps generate test
 
+# run it
+$ make up && make demo && make status
 
-    
-
-

Up and running

-

Coming soon...

+ Name Command State Ports +------------------------------------------------------------------------------------------ +env_db_1 docker-entrypoint.sh postgres Up 0.0.0.0:5432->5432/tcp +env_migration_1 form-api migrate up Exit 0 +env_server_1 /bin/sh -c envsubst '$SERV ... Up 80/tcp, 0.0.0.0:8080->8080/tcp +env_service_1 form-api run --with-profiler Up 0.0.0.0:8081->8080/tcp + +# check it +$ curl http://localhost:8080/api/v1/41ca5e09-3ce2-4094-b108-3ecc257c6fa4 +$ curl -H "Content-Type: application/x-www-form-urlencoded" \ + --data-urlencode "email=test@my.email" \ + http://localhost:8080/api/v1/41ca5e09-3ce2-4094-b108-3ecc257c6fa4
+
+
Requirements
+
    +
  • Docker 17.09.0-ce or above
  • +
  • Docker Compose 1.16.1 or above
  • +
  • Go 1.9.2 or above
  • +
  • GNU Make 3.81 or above
  • +
+
+
+ Installation from source code +
+
+
+
+

using Docker

+
+
$ docker network create -d bridge form-api
+$ docker run --rm -d \
+             --env-file .env \
+             --network form-api \
+             --name form-api-db \
+             -h db \
+             -p 5432:5432 \
+             postgres:10-alpine
+$ docker run --rm -it \
+             --env-file .env \
+             --network form-api \
+             --name form-api-migration \
+             --link form-api-db:db \
+             -h migration \
+             kamilsk/form-api:latest migrate up --demo
+$ docker run --rm -d \
+             --env-file .env \
+             --network form-api \
+             --name form-api-service \
+             --link form-api-db:db \
+             -h service \
+             -p 8080:8080 \
+             kamilsk/form-api:latest
+
+
Images
+ +
+
+ Using Docker +
+
+
+
+

and now you are ready to taste it

+
@@ -303,61 +409,53 @@

Up and running

-

- Roadmap
- -

+

Roadmap

-
+

MVP, v1.0

-

...

- +

CRUD, v2.0

-

...

- +

GUI, v3.0

-

...

- +

API v2, v4.0

-

...

- +

Extensibility, v5.0

-

...

@@ -367,7 +465,6 @@

Extensibility, v5.0

FormA, SaaS

-

...

@@ -385,6 +482,7 @@

FormA, SaaS

Feel free to support us at

Patreon +
@@ -394,6 +492,15 @@

FormA, SaaS

- + + diff --git a/docs/script.js b/docs/script.js new file mode 100644 index 0000000..d9d7c48 --- /dev/null +++ b/docs/script.js @@ -0,0 +1,55 @@ +(function ($) { + 'use strict'; + const + location = new URL(window.location.href), + messages = $('#messages'), tpl = $('#message'), + success = 'success', + failure = 'failure'; + var hashMap = {}; + + function showMessage(id, value) { + const node = $('#' + id); + var type, title, desc, message; + switch (value) { + case success: + type = 'alert-success'; + title = '"' + node.attr('title') + '" form was processed successfully!'; + desc = 'Aww yeah, you did the right thing!'; + break; + case failure: + type = 'alert-danger'; + title = '"' + node.attr('title') + '" form processed unsuccessfully'; + desc = 'Oops! But this also happens 😉'; + break; + default: + return; + } + message = $(tpl.html() + .replace('{{ type }}', type) + .replace('{{ title }}', title) + .replace('{{ desc }}', desc)); + messages.append(message); + setTimeout(function () { message.alert('close'); }, 4000 + 1000 * Math.random()); + } + + function showMessages() { + for (var id in hashMap) { + if (hashMap.hasOwnProperty(id)) { showMessage(id, hashMap[id]); } + } + hashMap = {} + } + + $('.example form').each(function (i, node) { + const value = location.searchParams.get(node.id); + if (value) { hashMap[node.id] = value } + }); + showMessages(); + + $('.clipboard .btn-alert').on('click', function (e) { + e.preventDefault(); + $(this).parent().prev().find('form[id]').each(function (i, node) { + hashMap[node.id] = Math.round(Math.random()) ? success : failure; + }); + showMessages(); + }); +}(window.jQuery)); diff --git a/docs/style.css b/docs/style.css index 872bfc2..9556d75 100644 --- a/docs/style.css +++ b/docs/style.css @@ -1,71 +1,64 @@ -.msg { position: fixed; bottom: 0; right: 1rem; } -.ribbon img { position: absolute; top: 0; right: 0; border: 0; } -.product-device { - position: absolute; - right: 10%; - bottom: -30%; - width: 300px; - height: 540px; - background-color: #333; - border-radius: 21px; - -webkit-transform: rotate(30deg); +footer { + padding: 2.5rem 0; + background-color: #f9f9f9; border-top: .05rem solid #e5e5e5; + color: #999; text-align: center; +} +img { max-width: 100%; } + +.clipboard { + display: none; + position: relative; float: right; +} +.clipboard .btn-alert { + display: block; + position: absolute; top: .5rem; right: .5rem; z-index: 10; + padding: .25rem .5rem; + background-color: transparent; border: 0; border-radius: .25rem; + color: #818a91; font-size: 75%; + cursor: pointer; +} +.clipboard .btn-alert:hover { + color: #fff; + background-color: #027de7; +} +.device { + position: absolute; right: 10%; bottom: -30%; + width: 300px; height: 540px; + background-color: #333; border-radius: 21px; transform: rotate(30deg); } -.product-device::before { - position: absolute; - top: 10%; - right: 10px; - bottom: 10%; - left: 10px; +.device::before { content: ""; - background-color: rgba(255, 255, 255, .1); - border-radius: 5px; -} -.product-device-2 { - top: -25%; - right: auto; - bottom: 0; - left: 5%; + position: absolute; top: 10%; right: 10px; bottom: 10%; left: 10px; + background-color: rgba(255, 255, 255, .1); border-radius: 5px; +} +.device-2 { + top: -25%; right: auto; bottom: 0; left: 5%; background-color: #e5e5e5; } -.overflow-hidden { overflow: hidden; } .example { position: relative; - padding: 1rem; - margin: 10px 0 0; - border: solid #f7f7f9; - border-width: .2rem 0 0; -} -@media (min-width: 576px) { - .example { - padding: 1.5rem; - border-width: .2rem; - } -} -.example .title { position: absolute; top: 12px; right: 12px; padding: 9px; } -.highlight { margin: 0 0 40px; padding: 1rem; background-color: #f7f7f9; } -.highlight pre { margin: 0; padding: 0; } -.highlight code { margin: 0; padding: 0; } -footer { - padding: 2.5rem 0; - color: #999; - text-align: center; - background-color: #f9f9f9; - border-top: .05rem solid #e5e5e5; -} + margin: 10px 0 0; padding: 1rem; + border: solid #f7f7f9; border-width: .2rem 0 0; +} +.example .title { + position: absolute; top: 12px; right: 12px; + padding: 9px; +} +.highlight { padding: 1rem; background-color: #f7f7f9; } +.highlight pre, +.highlight code { margin: 0; padding: 0; } +.overflow-hidden { overflow: hidden; } +.ribbon img { position: absolute; top: 0; right: 0; } + +#messages { position: fixed; bottom: 1rem; right: 1rem; z-index: 10; width: 40%; max-width: 512px; } + + + + -.cd-container { - width: 90%; - max-width: 1170px; - margin: 0 auto; -} -.cd-container::after { - content: ''; - display: table; - clear: both; -} #cd-timeline { position: relative; padding: 2em 0; @@ -81,16 +74,6 @@ footer { width: 4px; background: #d7e4ed; } -@media only screen and (min-width: 1170px) { - #cd-timeline { - margin-top: 3em; - margin-bottom: 3em; - } - #cd-timeline::before { - left: 50%; - margin-left: -2px; - } -} .cd-timeline-block { position: relative; margin: 2em 0; @@ -106,17 +89,6 @@ footer { .cd-timeline-block:last-child { margin-bottom: 0; } -@media only screen and (min-width: 1170px) { - .cd-timeline-block { - margin: 4em 0; - } - .cd-timeline-block:first-child { - margin-top: 0; - } - .cd-timeline-block:last-child { - margin-bottom: 0; - } -} .cd-timeline-img { position: absolute; top: 0; @@ -140,14 +112,6 @@ footer { .cd-timeline-img.overdue { background: #c03b44; } -@media only screen and (min-width: 1170px) { - .cd-timeline-img { - width: 60px; - height: 60px; - left: 50%; - margin-left: -30px; - } -} .cd-timeline-content { position: relative; margin-left: 60px; @@ -182,9 +146,30 @@ footer { height: 0; width: 0; border: 7px solid transparent; - border-right: 7px solid white; + border-right: 7px solid #f7f7f9; +} + + + + + + + +@media only screen and (min-width: 576px) { + .example { + padding: 1.5rem; + border-width: .2rem; + } } @media only screen and (min-width: 768px) { + .clipboard { display: block; } + + + + + + + .cd-timeline-content h2 { font-size: 1.25rem; } @@ -193,6 +178,29 @@ footer { } } @media only screen and (min-width: 1170px) { + #cd-timeline { + margin-top: 3em; + margin-bottom: 3em; + } + #cd-timeline::before { + left: 50%; + margin-left: -2px; + } + .cd-timeline-block { + margin: 4em 0; + } + .cd-timeline-block:first-child { + margin-top: 0; + } + .cd-timeline-block:last-child { + margin-bottom: 0; + } + .cd-timeline-img { + width: 60px; + height: 60px; + left: 50%; + margin-left: -30px; + } .cd-timeline-content { margin-left: 0; padding: 1.6em; @@ -202,7 +210,7 @@ footer { top: 24px; left: 100%; border-color: transparent; - border-left-color: white; + border-left-color: #f7f7f9; } .cd-timeline-block:nth-child(even) .cd-timeline-content { float: right; @@ -212,6 +220,6 @@ footer { left: auto; right: 100%; border-color: transparent; - border-right-color: white; + border-right-color: #f7f7f9; } } diff --git a/env/docker-compose.mk b/env/docker-compose.mk index 890c05e..43f2c46 100644 --- a/env/docker-compose.mk +++ b/env/docker-compose.mk @@ -98,8 +98,8 @@ logs-service: docker-compose .PHONY: demo -demo: - make docker-compose COMMAND='exec service form-api migrate --demo up' +demo: COMMAND = exec service form-api migrate --demo up +demo: docker-compose diff --git a/env/docker-compose.yml b/env/docker-compose.yml index fe9b463..d2996ff 100644 --- a/env/docker-compose.yml +++ b/env/docker-compose.yml @@ -21,7 +21,7 @@ services: env_file: .env restart: on-failure - server: # https://hub.docker.com/_/nginx/ + server: # https://hub.docker.com/r/kamilsk/nginx/ image: kamilsk/nginx:alpine entrypoint: | /bin/sh -c "envsubst '$$SERVICE_HOST $$SERVICE_PORT $$DOMAIN_NAME $$DOMAIN_PORT' \ diff --git a/env/rest.http b/env/rest.http index 53f2ebf..ce90073 100644 --- a/env/rest.http +++ b/env/rest.http @@ -34,6 +34,10 @@ Content-Type: application/x-www-form-urlencoded email=test@my.email +### + +GET http://localhost:8080/api/v1/41ca5e09-3ce2-4094-b108-3ecc257c6fa4/heartbeat + ### API v2 POST http://admin:admin@localhost:8080/api/v2/schema diff --git a/server/middleware/encoder.go b/server/middleware/encoder.go index 0321a29..bcac07f 100644 --- a/server/middleware/encoder.go +++ b/server/middleware/encoder.go @@ -11,6 +11,7 @@ import ( // Encoder injects required response encoder to the request context. func Encoder(next http.HandlerFunc) http.HandlerFunc { return func(rw http.ResponseWriter, req *http.Request) { + // Accept: */* // Accept: text/html // Accept: image/* // Accept: text/html, application/xhtml+xml, application/xml; q=0.9, */*; q=0.8 @@ -25,7 +26,7 @@ func Encoder(next http.HandlerFunc) http.HandlerFunc { } func fallback(value string, fallbackValues ...string) string { - if value == "" { + if value == "" || value == "*/*" { for _, value := range fallbackValues { if value != "" { return value diff --git a/server/router/chi/router.go b/server/router/chi/router.go index de8e371..ab0ca83 100644 --- a/server/router/chi/router.go +++ b/server/router/chi/router.go @@ -19,8 +19,10 @@ func NewRouter(api router.Server, withProfiler bool) http.Handler { r.Use(middleware.RealIP) r.Use(middleware.Logger) + notImplemented := func(rw http.ResponseWriter, req *http.Request) { rw.WriteHeader(http.StatusNotImplemented) } + r.Route("/api/v1", func(r chi.Router) { - r.Post("/", func(rw http.ResponseWriter, req *http.Request) { rw.WriteHeader(http.StatusNotImplemented) }) + r.Post("/", notImplemented) r.Route("/{UUID}", func(r chi.Router) { r.Use(func(next http.Handler) http.Handler { @@ -30,16 +32,18 @@ func NewRouter(api router.Server, withProfiler bool) http.Handler { }) r.Get("/", common.Encoder(api.GetV1)) - r.Put("/", func(rw http.ResponseWriter, req *http.Request) { rw.WriteHeader(http.StatusNotImplemented) }) - r.Delete("/", func(rw http.ResponseWriter, req *http.Request) { rw.WriteHeader(http.StatusNotImplemented) }) + r.Put("/", notImplemented) + r.Delete("/", notImplemented) r.Post("/", api.PostV1) + + r.Get("/heartbeat", notImplemented) }) }) r.Route("/api/v2", func(r chi.Router) { r.Route("/schema", func(r chi.Router) { - r.Post("/", func(rw http.ResponseWriter, req *http.Request) { rw.WriteHeader(http.StatusNotImplemented) }) + r.Post("/", notImplemented) r.Route("/{UUID}", func(r chi.Router) { r.Use(func(next http.Handler) http.Handler { @@ -48,16 +52,16 @@ func NewRouter(api router.Server, withProfiler bool) http.Handler { }) }) - r.Get("/", func(rw http.ResponseWriter, req *http.Request) { rw.WriteHeader(http.StatusNotImplemented) }) - r.Put("/", func(rw http.ResponseWriter, req *http.Request) { rw.WriteHeader(http.StatusNotImplemented) }) - r.Delete("/", func(rw http.ResponseWriter, req *http.Request) { rw.WriteHeader(http.StatusNotImplemented) }) + r.Get("/", notImplemented) + r.Put("/", notImplemented) + r.Delete("/", notImplemented) - r.Post("/", func(rw http.ResponseWriter, req *http.Request) { rw.WriteHeader(http.StatusNotImplemented) }) + r.Post("/", notImplemented) }) }) r.Route("/template", func(r chi.Router) { - r.Post("/", func(rw http.ResponseWriter, req *http.Request) { rw.WriteHeader(http.StatusNotImplemented) }) + r.Post("/", notImplemented) r.Route("/{UUID}", func(r chi.Router) { r.Use(func(next http.Handler) http.Handler { @@ -66,9 +70,9 @@ func NewRouter(api router.Server, withProfiler bool) http.Handler { }) }) - r.Get("/", func(rw http.ResponseWriter, req *http.Request) { rw.WriteHeader(http.StatusNotImplemented) }) - r.Put("/", func(rw http.ResponseWriter, req *http.Request) { rw.WriteHeader(http.StatusNotImplemented) }) - r.Delete("/", func(rw http.ResponseWriter, req *http.Request) { rw.WriteHeader(http.StatusNotImplemented) }) + r.Get("/", notImplemented) + r.Put("/", notImplemented) + r.Delete("/", notImplemented) }) }) }) @@ -85,7 +89,7 @@ func NewRouter(api router.Server, withProfiler bool) http.Handler { }) }) - r.Get("/", func(rw http.ResponseWriter, req *http.Request) { rw.WriteHeader(http.StatusNotImplemented) }) + r.Get("/", notImplemented) }) if withProfiler { diff --git a/server/router/chi/router_test.go b/server/router/chi/router_test.go index b01076f..74fdda8 100644 --- a/server/router/chi/router_test.go +++ b/server/router/chi/router_test.go @@ -66,6 +66,11 @@ func TestChiRouter(t *testing.T) { Do(func(rw http.ResponseWriter, _ *http.Request) { rw.WriteHeader(http.StatusFound) }) return request }, http.StatusFound}, + {"GET /api/v1/{UUID}/heartbeat", func() *http.Request { + return &http.Request{Method: http.MethodGet, URL: &url.URL{ + Scheme: "http", Host: "dev", Path: "/api/v1/" + UUID.String() + "/heartbeat", + }} + }, http.StatusNotImplemented}, {"POST /api/v2/schema", func() *http.Request { return &http.Request{Method: http.MethodPost, URL: &url.URL{