[
  {
    "path": ".gitignore",
    "content": "bin/\nvendor/\n\n# Binaries for programs and plugins\n*.exe\n*.exe~\n*.dll\n*.so\n*.dylib\n\n# Test binary, build with `go test -c`\n*.test\n\n# Output of the go coverage tool, specifically when used with LiteIDE\n*.out\n\n\n.DS_Store\n\n# temporarily do not track this\nPRACTICES.md\n\n# custom ignores\nexpt/\ntest.db\n.vscode/\n.idea/\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing to Droplets\n\nThanks for taking the time to contribute. You are Awesome! :heart:\n\nDroplets is built to showcase ideas, patterns and best practices which can be\napplied while building cool stuff with Go.\n\n## What can I contribute and How ?\n\nA project with realistic development cycle is required to be able to demonstrate\ndifferent ideas as applicable in real-world scenarios. This means, any type of\ncontribution that a typical open-source project would go through are welcome here.\n\nSome possible contributions:\n\n- Suggest features to add\n- Suggest improvements to code\n- Suggest improvements to documentation\n- Open an issue and discuss a practice used\n- Try the project and report bugs\n- Designs for the web app\n\nOr any other contribution you can think of (And be sure to send a PR to add it to\nthis document, which itself is another contribution!)\n\nYou can simply follow the guidelines from [opensource.guide](https://opensource.guide/how-to-contribute/).\n\n## Responsibilities\n\n* No platform specific code\n* Ensure that any code you add follows [CodeReviewComments](https://github.com/golang/go/wiki/CodeReviewComments), [EffectiveGo](https://golang.org/doc/effective_go.html) and [Clean Architecture](http://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html)\n* Keep PRs as small as possible to make it easy to review and discuss.\n* Be welcoming to newcomers and encourage diverse new contributors from all backgrounds.\n* Code change PRs must have unit tests\n\n"
  },
  {
    "path": "Dockerfile",
    "content": "FROM golang:1.11 as builder\nRUN mkdir /droplets-src\nWORKDIR /droplets-src\nCOPY ./ .\nRUN CGO_ENABLED=0 make setup all\n\nFROM alpine:latest\nRUN mkdir /app\nWORKDIR /app\nCOPY --from=builder /droplets-src/bin/droplets ./\nCOPY --from=builder /droplets-src/web ./web\nEXPOSE 8080\nCMD [\"./droplets\"]\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2018 Shivaprasad Bhat\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "Makefile",
    "content": "build:\n\t@echo \"Building droplets at './bin/droplets' ...\"\n\t@go build -o bin/droplets\n\nclean:\n\trm -rf ./bin\n\nall: lint\tvet\tcyclo\ttest\tbuild\n\ntest:\n\t@echo \"Running unit tests...\"\n\t@go test -cover ./...\n\ncyclo:\n\t@echo \"Checking cyclomatic complexity...\"\n\t@gocyclo -over 7 ./\n\nvet:\n\t@echo \"Running vet...\"\n\t@go vet ./...\n\nlint:\n\t@echo \"Running golint...\"\n\t@golint ./...\n\nsetup:\n\t@go get -u golang.org/x/lint/golint\n\t@go get -u github.com/fzipp/gocyclo"
  },
  {
    "path": "README.md",
    "content": "> WIP\n\n# droplets\n\n[![GoDoc](https://godoc.org/github.com/spy16/droplets?status.svg)](https://godoc.org/github.com/spy16/droplets) [![Go Report Card](https://goreportcard.com/badge/github.com/spy16/droplets)](https://goreportcard.com/report/github.com/spy16/droplets)\n[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fspy16%2Fdroplets.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2Fspy16%2Fdroplets?ref=badge_shield)\n\nA platform for Gophers similar to the awesome [Golang News](http://golangnews.com).\n\n## Why?\n\nDroplets is built to showcase:\n\n1. Application of [CodeReviewComments](https://github.com/golang/go/wiki/CodeReviewComments) and [EffectiveGo](https://golang.org/doc/effective_go.html)\n2. Usage of [Clean Architecture](http://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html)\n3. Testing practices such as [Table-driven tests](https://github.com/golang/go/wiki/TableDrivenTests)\n\nFollow the links to understand best practices and conventions used:\n1. [Directory Structure](./docs/organization.md)\n2. [Interfaces](./docs/interfaces.md)\n\n## Building\n\nDroplets uses `go mod` (available from `go 1.11`) for dependency management.\n\nTo test and build, run `make all`.\n\n## License\n[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fspy16%2Fdroplets.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2Fspy16%2Fdroplets?ref=badge_large)\n"
  },
  {
    "path": "docker-compose.yml",
    "content": "version: '3.2'\n\nservices:\n  droplets:\n    build: ./\n    environment:\n      - MONGO_URI=mongodb://mongo\n      - LOG_LEVEL=info\n    ports:\n      - \"8080:8080\"\n    links:\n      - mongo\n    networks:\n      - droplets_net\n  mongo:\n    image: mongo:3-stretch\n    ports:\n      - \"27017:27017\"\n    networks:\n      - droplets_net\n\nnetworks:\n  droplets_net:\n"
  },
  {
    "path": "docs/interfaces.md",
    "content": "\n# Interfaces\n\nFollowing are some best practices for using interfaces:\n\n1. Define small interfaces with well defined scope\n   - Single-method interfaces are ideal (e.g. `io.Reader`, `io.Writer` etc.)\n   - [Bigger the interface, weaker the abstraction - Go Proverbs by Rob Pike](https://www.youtube.com/watch?v=PAAkCSZUG1c&t=5m17s)\n2. Accept interfaces, return structs\n   - Interfaces should be defined where they are used [Read More](#where-should-i-define-the-interface-)\n\n## Where should I define the interface ?\n\nFrom [CodeReviewComments](https://github.com/golang/go/wiki/CodeReviewComments#interfaces):\n\n> Go interfaces generally belong in the package that uses values of the interface type, not the\n> package that implements those values.\n\nInterfaces are contracts that should be used to define the minimal requirement of a client\nto execute its functionality. In other words, client defines what it needs and not the\nimplementor. So, interfaces should generally be defined on the client side. This is also inline\nwith the [Interface Segregation Principle](https://en.wikipedia.org/wiki/Interface_segregation_principle)\nfrom [SOLID](https://en.wikipedia.org/wiki/SOLID) principles.\n\nA **bad** pattern that shows up quite a lot:\n\n```go\npackage producer\n\nfunc NewThinger() Thinger {\n    return defaultThinger{ … }\n}\n\ntype Thinger interface {\n    Thing() bool\n}\n\n\ntype defaultThinger struct{ … }\nfunc (t defaultThinger) Thing() bool { … }\n```\n\n### Why is this bad?\n\nGo uses [Structural Type System](https://en.wikipedia.org/wiki/Structural_type_system) as opposed to\n[Nominal Type System](https://en.wikipedia.org/wiki/Nominal_type_system) used in other static languages\nlike `Java`, `C#` etc. This simply means that a type `MyType` does not need to add `implements Doer` clause\nto be compatible with an interface `Doer`. `MyType` is compatible with `Doer` interface if it has all the\nmethods defined in `Doer`.\n\nRead following articles for more information:\n\n1. https://medium.com/@cep21/preemptive-interface-anti-pattern-in-go-54c18ac0668a\n2. https://medium.com/@cep21/what-accept-interfaces-return-structs-means-in-go-2fe879e25ee8\n\nThis also provides an interesting power to Go interfaces. Clients are truly free to define interfaces when they\nneed to. For example consider the following function:\n\n```go\nfunc writeData(f *os.File, data string) {\n    f.Write([]byte(data))\n}\n```\n\nLet's assume after sometime a new feature requirement which requires us to write to a tcp connection. One\nthing we could do is define a new function:\n\n```go\nfunc writeDataToTCPCon(con *net.TCPConn, data string) {\n    con.Write([]byte(data))\n}\n```\n\nBut this approach is tedious and will grow out of control quickly as new requirements are added. Also, different\nwriters cannot be injected into other entities easily. But instead, you can simply refactor the `writeData` function\nas below:\n\n```go\ntype writer interface {\n    Write([]byte) (int, error)\n}\n\nfunc writeData(wr writer, data string) {\n    wr.Write([]byte(data))\n}\n```\n\nRefactored `writeData` will continue to work with our existing code that is passing `*os.File` since it\nimplements `writer`. In addition, `writeData` function can now accept anything that implements `writer`\nwhich includes `os.File`, `net.TCPConn`, `http.ResponseWriter` etc. (And every single Go entity in the\n**entire world** that has a method `Write([]byte) (int, error)`)\n\nNote that, this pattern is *not possible in other languages*. Because, after refactoring `writeData` to\naccept a new interface `writer`, you need to refactor all the classes you want to use with `writeData` to\nhave `implements writer` in their declarations.\n\nAnother advantage is that client is free to define the subset of features it requires instead of accepting\nmore than it needs.\n\n\n"
  },
  {
    "path": "docs/organization.md",
    "content": "\n# Directory Structure\n\nDirectory structure is based on [Clean Architecture](http://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html).\n\n![Clean Architecture](http://blog.cleancoder.com/uncle-bob/images/2012-08-13-the-clean-architecture/CleanArchitecture.jpg)\n\nMost important part of Clean Architecture is the **Dependency Rule**:\n\n> **source code dependencies can only point inwards**\n\n### 1. `domain/`\n\nPackage `domain` represents the `entities` layer from the Clean Architecture. Entities in this layer are\n*least likely to change when something external changes*. For example, the rules inside `Validate` methods\nare designed in such a way that the these are *absolute requirement* for the entity to belong to the domain.\nFor example, a `User` object without `Name` does not belong to `domain` of Droplets.\n\nIn other words, any feature or business requirement that comes in later, these definitions would still not\nchange unless the requirement is leading to a domain change.\n\nThis package **strictly cannot** have direct dependency on external packages. It can use built-in types,\nfunctions and standard library types/functions.\n\n> `domain` package makes one exception and imports `github.com/spy16/droplets/pkg/errors`. This is because\n> of errors are values and are a basic requirement across the application. In other words, the `errors` package\n> is used in place of `errors` package from the standard library.\n\n### 2. `usecases/`\n\nDirectory (not a package) `usecases` represents the `Use Cases` layer from the Clean Architecture. It encapsulates\nand implements all of the use cases of the system by directing the `entities` layer. This layer is expected to change\nwhen a new use case or business requirement is presented.\n\nIn Droplets, Use cases are separated as packages based on the entity they primarily operate on. (e.g. `usecases/users` etc.)\n\nAny real use case would also need external entities such as persistence, external service integration etc.\nBut this layer also **strictly** cannot have direct dependency on external packages. This crossing of boundaries\nis done through interfaces.\n\nFor example, `users.Registrar` provides functions for registering users which requires storage functionality. But\nthis cannot directly import a `mongo` or `sql` driver and implement storage functions. It can also not import an\nadapter from the `interfaces` package directly both of which would violate `Dependency Rule`. So instead, an interface\n`users.Store` is defined which is expected to injected when calling `NewRegistrar`.\n\n**Why is `Store` interface defined in `users` package?**\n\nSee [Interfaces](interfaces.md) for conventions around interfaces.\n\n\n### 3. `interfaces/`\n\n> Should not be confused with Go `interface` keyword.\n\n- Represents the `interface-adapter` layer from the Clean Architecture\n- This is the layer that cares about the external world (i.e, external dependencies).\n- Interfacing includes:\n    - Exposing `usecases` as API (e.g., RPC, GraphQL, REST etc.)\n    - Presenting `usecases` to end-user (e.g., GUI, WebApp etc.)\n    - Persistence logic (e.g., cache, datastores etc.)\n    - Integrating an external service required by `usecases`\n- Packages inside this are organized in 2 ways:\n    1. Based on the medium they use (e.g., `rest`, `web` etc.)\n    2. Based on the external dependency they use (e.g., `mongo`, `redis` etc.)\n\n### 4. `pkg/`\n\n- Contains re-usable packages that is safe to be imported in other projects\n- This package should not import anything from `domain`, `interfaces`, `usecases` or their sub-packages\n\n\n### 5. `web/`\n\n- `web/` is **NOT** a Go package\n- Contains web assets such as css, images, templates etc."
  },
  {
    "path": "domain/doc.go",
    "content": "// Package domain contains domain entities and core validation rules.\npackage domain\n"
  },
  {
    "path": "domain/meta.go",
    "content": "package domain\n\nimport (\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/spy16/droplets/pkg/errors\"\n)\n\n// Meta represents metadata about different entities.\ntype Meta struct {\n\t// Name represents a unique name/identifier for the object.\n\tName string `json:\"name\" bson:\"name\"`\n\n\t// Tags can contain additional metadata about the object.\n\tTags []string `json:\"tags,omitempty\" bson:\"tags\"`\n\n\t// CreateAt represents the time at which this object was created.\n\tCreatedAt time.Time `json:\"created_at,omitempty\" bson:\"created_at\"`\n\n\t// UpdatedAt represents the time at which this object was last\n\t// modified.\n\tUpdatedAt time.Time `json:\"updated_at,omitempty\" bson:\"updated_at\"`\n}\n\n// SetDefaults sets sensible defaults on meta.\nfunc (meta *Meta) SetDefaults() {\n\tif meta.CreatedAt.IsZero() {\n\t\tmeta.CreatedAt = time.Now()\n\t\tmeta.UpdatedAt = time.Now()\n\t}\n}\n\n// Validate performs basic validation of the metadata.\nfunc (meta Meta) Validate() error {\n\tswitch {\n\tcase empty(meta.Name):\n\t\treturn errors.MissingField(\"Name\")\n\t}\n\treturn nil\n}\n\nfunc empty(str string) bool {\n\treturn len(strings.TrimSpace(str)) == 0\n}\n"
  },
  {
    "path": "domain/meta_test.go",
    "content": "package domain_test\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/spy16/droplets/domain\"\n\t\"github.com/spy16/droplets/pkg/errors\"\n)\n\nfunc TestMeta_Validate(suite *testing.T) {\n\tsuite.Parallel()\n\n\tcases := []struct {\n\t\tmeta      domain.Meta\n\t\texpectErr bool\n\t\terrType   string\n\t}{}\n\n\tfor id, cs := range cases {\n\t\tsuite.Run(fmt.Sprintf(\"Case#%d\", id), func(t *testing.T) {\n\t\t\ttestValidation(t, cs.meta, cs.expectErr, cs.errType)\n\t\t})\n\t}\n\n}\n\nfunc testValidation(t *testing.T, validator validatable, expectErr bool, errType string) {\n\terr := validator.Validate()\n\tif err != nil {\n\t\tif !expectErr {\n\t\t\tt.Errorf(\"unexpected error: %s\", err)\n\t\t\treturn\n\t\t}\n\n\t\tif actualType := errors.Type(err); actualType != errType {\n\t\t\tt.Errorf(\"expecting error type '%s', got '%s'\", errType, actualType)\n\t\t}\n\t\treturn\n\t}\n\n\tif expectErr {\n\t\tt.Errorf(\"was expecting an error of type '%s', got nil\", errType)\n\t\treturn\n\t}\n}\n\ntype validatable interface {\n\tValidate() error\n}\n"
  },
  {
    "path": "domain/post.go",
    "content": "package domain\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/spy16/droplets/pkg/errors\"\n)\n\n// Common content types.\nconst (\n\tContentLibrary = \"library\"\n\tContentLink    = \"link\"\n\tContentVideo   = \"video\"\n)\n\nvar validTypes = []string{ContentLibrary, ContentLink, ContentVideo}\n\n// Post represents an article, link, video etc.\ntype Post struct {\n\tMeta `json:\",inline\" bson:\",inline\"`\n\n\t// Type should state the type of the content. (e.g., library,\n\t// video, link etc.)\n\tType string `json:\"type\" bson:\"type\"`\n\n\t// Body should contain the actual content according to the Type\n\t// specified. (e.g. github.com/spy16/parens when Type=link)\n\tBody string `json:\"body\" bson:\"body\"`\n\n\t// Owner represents the name of the user who created the post.\n\tOwner string `json:\"owner\" bson:\"owner\"`\n}\n\n// Validate performs validation of the post.\nfunc (post Post) Validate() error {\n\tif err := post.Meta.Validate(); err != nil {\n\t\treturn err\n\t}\n\n\tif len(strings.TrimSpace(post.Body)) == 0 {\n\t\treturn errors.MissingField(\"Body\")\n\t}\n\n\tif len(strings.TrimSpace(post.Owner)) == 0 {\n\t\treturn errors.MissingField(\"Owner\")\n\t}\n\n\tif !contains(post.Type, validTypes) {\n\t\treturn errors.InvalidValue(\"Type\", fmt.Sprintf(\"type must be one of: %s\", strings.Join(validTypes, \",\")))\n\t}\n\n\treturn nil\n}\n\nfunc contains(val string, vals []string) bool {\n\tfor _, item := range vals {\n\t\tif val == item {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n"
  },
  {
    "path": "domain/post_test.go",
    "content": "package domain_test\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/spy16/droplets/domain\"\n)\n\nfunc TestPost_Validate(suite *testing.T) {\n\tsuite.Parallel()\n\n\tvalidMeta := domain.Meta{\n\t\tName: \"hello\",\n\t}\n\n\tcases := []struct {\n\t\tpost      domain.Post\n\t\texpectErr bool\n\t}{\n\t\t{\n\t\t\tpost:      domain.Post{},\n\t\t\texpectErr: true,\n\t\t},\n\t\t{\n\t\t\tpost: domain.Post{\n\t\t\t\tMeta: validMeta,\n\t\t\t},\n\t\t\texpectErr: true,\n\t\t},\n\t\t{\n\t\t\tpost: domain.Post{\n\t\t\t\tMeta: validMeta,\n\t\t\t\tBody: \"hello world post!\",\n\t\t\t},\n\t\t\texpectErr: true,\n\t\t},\n\t\t{\n\t\t\tpost: domain.Post{\n\t\t\t\tMeta:  validMeta,\n\t\t\t\tType:  \"blah\",\n\t\t\t\tOwner: \"spy16\",\n\t\t\t\tBody:  \"hello world post!\",\n\t\t\t},\n\t\t\texpectErr: true,\n\t\t},\n\t\t{\n\t\t\tpost: domain.Post{\n\t\t\t\tMeta: validMeta,\n\t\t\t\tType: domain.ContentLibrary,\n\t\t\t\tBody: \"hello world post!\",\n\t\t\t},\n\t\t\texpectErr: true,\n\t\t},\n\t\t{\n\t\t\tpost: domain.Post{\n\t\t\t\tMeta:  validMeta,\n\t\t\t\tType:  domain.ContentLibrary,\n\t\t\t\tBody:  \"hello world post!\",\n\t\t\t\tOwner: \"spy16\",\n\t\t\t},\n\t\t\texpectErr: false,\n\t\t},\n\t}\n\n\tfor id, cs := range cases {\n\t\tsuite.Run(fmt.Sprintf(\"#%d\", id), func(t *testing.T) {\n\t\t\terr := cs.post.Validate()\n\t\t\tif err != nil {\n\t\t\t\tif !cs.expectErr {\n\t\t\t\t\tt.Errorf(\"was not expecting error, got '%s'\", err)\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif cs.expectErr {\n\t\t\t\tt.Errorf(\"was expecting error, got nil\")\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "domain/user.go",
    "content": "package domain\n\nimport (\n\t\"net/mail\"\n\n\t\"github.com/spy16/droplets/pkg/errors\"\n\t\"golang.org/x/crypto/bcrypt\"\n)\n\n// User represents information about registered users.\ntype User struct {\n\tMeta `json:\",inline,omitempty\" bson:\",inline\"`\n\n\t// Email should contain a valid email of the user.\n\tEmail string `json:\"email,omitempty\" bson:\"email\"`\n\n\t// Secret represents the user secret.\n\tSecret string `json:\"secret,omitempty\" bson:\"secret\"`\n}\n\n// Validate performs basic validation of user information.\nfunc (user User) Validate() error {\n\tif err := user.Meta.Validate(); err != nil {\n\t\treturn err\n\t}\n\n\t_, err := mail.ParseAddress(user.Email)\n\tif err != nil {\n\t\treturn errors.InvalidValue(\"Email\", err.Error())\n\t}\n\n\treturn nil\n}\n\n// HashSecret creates bcrypt hash of the password.\nfunc (user *User) HashSecret() error {\n\tbytes, err := bcrypt.GenerateFromPassword([]byte(user.Secret), 4)\n\tif err != nil {\n\t\treturn err\n\t}\n\tuser.Secret = string(bytes)\n\treturn nil\n}\n\n// CheckSecret compares the cleartext password with the hash.\nfunc (user User) CheckSecret(password string) bool {\n\terr := bcrypt.CompareHashAndPassword([]byte(user.Secret), []byte(password))\n\treturn err == nil\n}\n"
  },
  {
    "path": "domain/user_test.go",
    "content": "package domain_test\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/spy16/droplets/domain\"\n\t\"github.com/spy16/droplets/pkg/errors\"\n)\n\nfunc TestUser_CheckSecret(t *testing.T) {\n\tpassword := \"hello@world!\"\n\n\tuser := domain.User{}\n\tuser.Secret = password\n\terr := user.HashSecret()\n\tif err != nil {\n\t\tt.Errorf(\"was not expecting error, got '%s'\", err)\n\t}\n\n\tif !user.CheckSecret(password) {\n\t\tt.Errorf(\"CheckSecret expected to return true, but got false\")\n\t}\n}\n\nfunc TestUser_Validate(suite *testing.T) {\n\tsuite.Parallel()\n\n\tcases := []struct {\n\t\tuser      domain.User\n\t\texpectErr bool\n\t\terrType   string\n\t}{\n\t\t{\n\t\t\tuser:      domain.User{},\n\t\t\texpectErr: true,\n\t\t\terrType:   errors.TypeMissingField,\n\t\t},\n\t\t{\n\t\t\tuser: domain.User{\n\t\t\t\tMeta: domain.Meta{\n\t\t\t\t\tName: \"spy16\",\n\t\t\t\t},\n\t\t\t\tEmail: \"blah.com\",\n\t\t\t},\n\t\t\texpectErr: true,\n\t\t\terrType:   errors.TypeInvalidValue,\n\t\t},\n\t\t{\n\t\t\tuser: domain.User{\n\t\t\t\tMeta: domain.Meta{\n\t\t\t\t\tName: \"spy16\",\n\t\t\t\t},\n\t\t\t\tEmail: \"spy16 <no-mail@nomail.com>\",\n\t\t\t},\n\t\t\texpectErr: false,\n\t\t},\n\t}\n\n\tfor id, cs := range cases {\n\t\tsuite.Run(fmt.Sprintf(\"Case#%d\", id), func(t *testing.T) {\n\t\t\ttestValidation(t, cs.user, cs.expectErr, cs.errType)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "go.mod",
    "content": "module github.com/spy16/droplets\n\ngo 1.15\n\nrequire (\n\tgithub.com/BurntSushi/toml v0.3.1 // indirect\n\tgithub.com/gorilla/context v1.1.1 // indirect\n\tgithub.com/gorilla/mux v1.6.2\n\tgithub.com/kr/pretty v0.1.0 // indirect\n\tgithub.com/mitchellh/mapstructure v1.1.2 // indirect\n\tgithub.com/pelletier/go-toml v1.2.0\n\tgithub.com/sirupsen/logrus v1.2.0\n\tgithub.com/spf13/cast v1.3.0 // indirect\n\tgithub.com/spf13/pflag v1.0.3 // indirect\n\tgithub.com/spf13/viper v1.2.1\n\tgithub.com/unrolled/render v0.0.0-20180914162206-b9786414de4d\n\tgolang.org/x/crypto v0.0.0-20181106171534-e4dc69e5b2fd\n\tgolang.org/x/sys v0.0.0-20181116161606-93218def8b18 // indirect\n\tgopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect\n\tgopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce\n)\n"
  },
  {
    "path": "go.sum",
    "content": "github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=\ngithub.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=\ngithub.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=\ngithub.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=\ngithub.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=\ngithub.com/gorilla/mux v1.6.2 h1:Pgr17XVTNXAk3q/r4CpKzC5xBM/qW1uVLV+IhRZpIIk=\ngithub.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=\ngithub.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=\ngithub.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=\ngithub.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=\ngithub.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=\ngithub.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=\ngithub.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=\ngithub.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=\ngithub.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=\ngithub.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=\ngithub.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY=\ngithub.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=\ngithub.com/mitchellh/mapstructure v1.0.0/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=\ngithub.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=\ngithub.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=\ngithub.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=\ngithub.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/sirupsen/logrus v1.2.0 h1:juTguoYk5qI21pwyTXY3B3Y5cOTH3ZUyZCg1v/mihuo=\ngithub.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=\ngithub.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=\ngithub.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=\ngithub.com/spf13/cast v1.2.0/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg=\ngithub.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=\ngithub.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=\ngithub.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk=\ngithub.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=\ngithub.com/spf13/pflag v1.0.2/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=\ngithub.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=\ngithub.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=\ngithub.com/spf13/viper v1.2.1 h1:bIcUwXqLseLF3BDAZduuNfekWG87ibtFxi59Bq+oI9M=\ngithub.com/spf13/viper v1.2.1/go.mod h1:P4AexN0a+C9tGAnUFNwDMYYZv3pjFuvmeiMyKRaNVlI=\ngithub.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=\ngithub.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=\ngithub.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=\ngithub.com/unrolled/render v0.0.0-20180914162206-b9786414de4d h1:ggUgChAeyge4NZ4QUw6lhHsVymzwSDJOZcE0s2X8S20=\ngithub.com/unrolled/render v0.0.0-20180914162206-b9786414de4d/go.mod h1:tu82oB5W2ykJRVioYsB+IQKcft7ryBr7w12qMBUPyXg=\ngolang.org/x/crypto v0.0.0-20180904163835-0709b304e793 h1:u+LnwYTOOW7Ukr/fppxEb1Nwz0AtPflrblfvUudpo+I=\ngolang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=\ngolang.org/x/crypto v0.0.0-20181106171534-e4dc69e5b2fd h1:VtIkGDhk0ph3t+THbvXHfMZ8QHgsBO39Nh52+74pq7w=\ngolang.org/x/crypto v0.0.0-20181106171534-e4dc69e5b2fd/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=\ngolang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33 h1:I6FyU15t786LL7oL/hn43zqTuEGr4PN7F4XJ1p4E3Y8=\ngolang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20180906133057-8cf3aee42992 h1:BH3eQWeGbwRU2+wxxuuPOdFBmaiBH81O8BugSjHeTFg=\ngolang.org/x/sys v0.0.0-20180906133057-8cf3aee42992/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20181116161606-93218def8b18 h1:Wh+XCfg3kNpjhdq2LXrsiOProjtQZKme5XUx7VcxwAw=\ngolang.org/x/sys v0.0.0-20181116161606-93218def8b18/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=\ngopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce h1:xcEWjVhvbDy+nHP67nPDDpbYrY+ILlfndk4bRioVHaU=\ngopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA=\ngopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=\ngopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\n"
  },
  {
    "path": "interfaces/mongo/doc.go",
    "content": "// Package mongo contains any component in the entire project which interfaces\n// with MongoDB (e.g. different store implementations).\npackage mongo\n"
  },
  {
    "path": "interfaces/mongo/mongo.go",
    "content": "package mongo\n\nimport (\n\t\"gopkg.in/mgo.v2\"\n)\n\n// Connect to a MongoDB instance located by mongo-uri using the `mgo`\n// driver.\nfunc Connect(uri string, failFast bool) (*mgo.Database, func(), error) {\n\tdi, err := mgo.ParseURL(uri)\n\tif err != nil {\n\t\treturn nil, doNothing, err\n\t}\n\n\tdi.FailFast = failFast\n\tsession, err := mgo.DialWithInfo(di)\n\tif err != nil {\n\t\treturn nil, doNothing, err\n\t}\n\n\treturn session.DB(di.Database), session.Close, nil\n}\n\nfunc doNothing() {}\n"
  },
  {
    "path": "interfaces/mongo/posts.go",
    "content": "package mongo\n\nimport (\n\t\"context\"\n\t\"time\"\n\n\t\"github.com/spy16/droplets/domain\"\n\t\"github.com/spy16/droplets/pkg/errors\"\n\t\"gopkg.in/mgo.v2\"\n\t\"gopkg.in/mgo.v2/bson\"\n)\n\nconst colPosts = \"posts\"\n\n// NewPostStore initializes the Posts store with given mongo db handle.\nfunc NewPostStore(db *mgo.Database) *PostStore {\n\treturn &PostStore{\n\t\tdb: db,\n\t}\n}\n\n// PostStore manages persistence and retrieval of posts.\ntype PostStore struct {\n\tdb *mgo.Database\n}\n\n// Exists checks if a post exists by name.\nfunc (posts *PostStore) Exists(ctx context.Context, name string) bool {\n\tcol := posts.db.C(colPosts)\n\n\tcount, err := col.Find(bson.M{\"name\": name}).Count()\n\tif err != nil {\n\t\treturn false\n\t}\n\treturn count > 0\n}\n\n// Get finds a post by name.\nfunc (posts *PostStore) Get(ctx context.Context, name string) (*domain.Post, error) {\n\tcol := posts.db.C(colPosts)\n\n\tpost := domain.Post{}\n\tif err := col.Find(bson.M{\"name\": name}).One(&post); err != nil {\n\t\tif err == mgo.ErrNotFound {\n\t\t\treturn nil, errors.ResourceNotFound(\"Post\", name)\n\t\t}\n\t\treturn nil, errors.Wrapf(err, \"failed to fetch post\")\n\t}\n\n\tpost.SetDefaults()\n\treturn &post, nil\n}\n\n// Save validates and persists the post.\nfunc (posts *PostStore) Save(ctx context.Context, post domain.Post) (*domain.Post, error) {\n\tpost.SetDefaults()\n\tif err := post.Validate(); err != nil {\n\t\treturn nil, err\n\t}\n\tpost.CreatedAt = time.Now()\n\tpost.UpdatedAt = time.Now()\n\n\tcol := posts.db.C(colPosts)\n\tif err := col.Insert(post); err != nil {\n\t\treturn nil, err\n\t}\n\treturn &post, nil\n}\n\n// Delete removes one post identified by the name.\nfunc (posts *PostStore) Delete(ctx context.Context, name string) (*domain.Post, error) {\n\tcol := posts.db.C(colPosts)\n\n\tch := mgo.Change{\n\t\tRemove:    true,\n\t\tReturnNew: true,\n\t\tUpsert:    false,\n\t}\n\tpost := domain.Post{}\n\t_, err := col.Find(bson.M{\"name\": name}).Apply(ch, &post)\n\tif err != nil {\n\t\tif err == mgo.ErrNotFound {\n\t\t\treturn nil, errors.ResourceNotFound(\"Post\", name)\n\t\t}\n\t\treturn nil, err\n\t}\n\n\treturn &post, nil\n}\n"
  },
  {
    "path": "interfaces/mongo/users.go",
    "content": "package mongo\n\nimport (\n\t\"context\"\n\t\"time\"\n\n\t\"github.com/spy16/droplets/domain\"\n\t\"github.com/spy16/droplets/pkg/errors\"\n\t\"gopkg.in/mgo.v2\"\n\t\"gopkg.in/mgo.v2/bson\"\n)\n\nconst colUsers = \"users\"\n\n// NewUserStore initializes a users store with the given db handle.\nfunc NewUserStore(db *mgo.Database) *UserStore {\n\treturn &UserStore{\n\t\tdb: db,\n\t}\n}\n\n// UserStore provides functions for persisting User entities in MongoDB.\ntype UserStore struct {\n\tdb *mgo.Database\n}\n\n// Exists checks if the user identified by the given username already\n// exists. Will return false in case of any error.\nfunc (users *UserStore) Exists(ctx context.Context, name string) bool {\n\tcol := users.db.C(colUsers)\n\n\tcount, err := col.Find(bson.M{\"name\": name}).Count()\n\tif err != nil {\n\t\treturn false\n\t}\n\treturn count > 0\n}\n\n// Save validates and persists the user.\nfunc (users *UserStore) Save(ctx context.Context, user domain.User) (*domain.User, error) {\n\tuser.SetDefaults()\n\tif err := user.Validate(); err != nil {\n\t\treturn nil, err\n\t}\n\tuser.CreatedAt = time.Now()\n\tuser.UpdatedAt = time.Now()\n\n\tcol := users.db.C(colUsers)\n\tif err := col.Insert(user); err != nil {\n\t\treturn nil, err\n\t}\n\treturn &user, nil\n}\n\n// FindByName finds a user by name. If not found, returns ResourceNotFound error.\nfunc (users *UserStore) FindByName(ctx context.Context, name string) (*domain.User, error) {\n\tcol := users.db.C(colUsers)\n\n\tuser := domain.User{}\n\tif err := col.Find(bson.M{\"name\": name}).One(&user); err != nil {\n\t\tif err == mgo.ErrNotFound {\n\t\t\treturn nil, errors.ResourceNotFound(\"User\", name)\n\t\t}\n\t\treturn nil, errors.Wrapf(err, \"failed to fetch user\")\n\t}\n\n\tuser.SetDefaults()\n\treturn &user, nil\n}\n\n// FindAll finds all users matching the tags.\nfunc (users *UserStore) FindAll(ctx context.Context, tags []string, limit int) ([]domain.User, error) {\n\tcol := users.db.C(colUsers)\n\n\tfilter := bson.M{}\n\tif len(tags) > 0 {\n\t\tfilter[\"tags\"] = bson.M{\n\t\t\t\"$in\": tags,\n\t\t}\n\t}\n\n\tmatches := []domain.User{}\n\tif err := col.Find(filter).Limit(limit).All(&matches); err != nil {\n\t\treturn nil, errors.Wrapf(err, \"failed to query for users\")\n\t}\n\treturn matches, nil\n}\n\n// Delete removes one user identified by the name.\nfunc (users *UserStore) Delete(ctx context.Context, name string) (*domain.User, error) {\n\tcol := users.db.C(colUsers)\n\n\tch := mgo.Change{\n\t\tRemove:    true,\n\t\tReturnNew: true,\n\t\tUpsert:    false,\n\t}\n\tuser := domain.User{}\n\t_, err := col.Find(bson.M{\"name\": name}).Apply(ch, &user)\n\tif err != nil {\n\t\tif err == mgo.ErrNotFound {\n\t\t\treturn nil, errors.ResourceNotFound(\"User\", name)\n\t\t}\n\t\treturn nil, err\n\t}\n\n\treturn &user, nil\n}\n"
  },
  {
    "path": "interfaces/rest/doc.go",
    "content": "// Package rest exposes the features of droplets as REST API. This\n// package uses gorilla/mux for routing.\npackage rest\n"
  },
  {
    "path": "interfaces/rest/posts.go",
    "content": "package rest\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\n\t\"github.com/gorilla/mux\"\n\t\"github.com/spy16/droplets/domain\"\n\t\"github.com/spy16/droplets/pkg/logger\"\n\t\"github.com/spy16/droplets/pkg/middlewares\"\n\t\"github.com/spy16/droplets/usecases/posts\"\n)\n\nfunc addPostsAPI(router *mux.Router, pub postPublication, ret postRetriever, lg logger.Logger) {\n\tpc := &postController{}\n\tpc.ret = ret\n\tpc.pub = pub\n\tpc.Logger = lg\n\n\trouter.HandleFunc(\"/v1/posts\", pc.search).Methods(http.MethodGet)\n\trouter.HandleFunc(\"/v1/posts/{name}\", pc.get).Methods(http.MethodGet)\n\trouter.HandleFunc(\"/v1/posts/{name}\", pc.delete).Methods(http.MethodDelete)\n\trouter.HandleFunc(\"/v1/posts\", pc.post).Methods(http.MethodPost)\n}\n\ntype postController struct {\n\tlogger.Logger\n\n\tpub postPublication\n\tret postRetriever\n}\n\nfunc (pc *postController) search(wr http.ResponseWriter, req *http.Request) {\n\tposts, err := pc.ret.Search(req.Context(), posts.Query{})\n\tif err != nil {\n\t\trespondErr(wr, err)\n\t\treturn\n\t}\n\n\trespond(wr, http.StatusOK, posts)\n}\n\nfunc (pc *postController) get(wr http.ResponseWriter, req *http.Request) {\n\tname := mux.Vars(req)[\"name\"]\n\tpost, err := pc.ret.Get(req.Context(), name)\n\tif err != nil {\n\t\trespondErr(wr, err)\n\t\treturn\n\t}\n\n\trespond(wr, http.StatusOK, post)\n}\n\nfunc (pc *postController) post(wr http.ResponseWriter, req *http.Request) {\n\tpost := domain.Post{}\n\tif err := readRequest(req, &post); err != nil {\n\t\tpc.Warnf(\"failed to read user request: %s\", err)\n\t\trespond(wr, http.StatusBadRequest, err)\n\t\treturn\n\t}\n\tuser, _ := middlewares.User(req)\n\tpost.Owner = user\n\n\tpublished, err := pc.pub.Publish(req.Context(), post)\n\tif err != nil {\n\t\trespondErr(wr, err)\n\t\treturn\n\t}\n\n\trespond(wr, http.StatusCreated, published)\n}\n\nfunc (pc *postController) delete(wr http.ResponseWriter, req *http.Request) {\n\tname := mux.Vars(req)[\"name\"]\n\tpost, err := pc.pub.Delete(req.Context(), name)\n\tif err != nil {\n\t\trespondErr(wr, err)\n\t\treturn\n\t}\n\n\trespond(wr, http.StatusOK, post)\n}\n\ntype postRetriever interface {\n\tGet(ctx context.Context, name string) (*domain.Post, error)\n\tSearch(ctx context.Context, query posts.Query) ([]domain.Post, error)\n}\n\ntype postPublication interface {\n\tPublish(ctx context.Context, post domain.Post) (*domain.Post, error)\n\tDelete(ctx context.Context, name string) (*domain.Post, error)\n}\n"
  },
  {
    "path": "interfaces/rest/rest.go",
    "content": "package rest\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/spy16/droplets/pkg/render\"\n\n\t\"github.com/gorilla/mux\"\n\t\"github.com/spy16/droplets/pkg/errors\"\n\t\"github.com/spy16/droplets/pkg/logger\"\n)\n\n// New initializes the server with routes exposing the given usecases.\nfunc New(logger logger.Logger, reg registration, ret retriever, postsRet postRetriever, postPub postPublication) http.Handler {\n\t// setup router with default handlers\n\trouter := mux.NewRouter()\n\trouter.NotFoundHandler = http.HandlerFunc(notFoundHandler)\n\trouter.MethodNotAllowedHandler = http.HandlerFunc(methodNotAllowedHandler)\n\n\t// setup api endpoints\n\taddUsersAPI(router, reg, ret, logger)\n\taddPostsAPI(router, postPub, postsRet, logger)\n\n\treturn router\n}\n\nfunc notFoundHandler(wr http.ResponseWriter, req *http.Request) {\n\trender.JSON(wr, http.StatusNotFound, errors.ResourceNotFound(\"path\", req.URL.Path))\n}\n\nfunc methodNotAllowedHandler(wr http.ResponseWriter, req *http.Request) {\n\trender.JSON(wr, http.StatusMethodNotAllowed, errors.ResourceNotFound(\"path\", req.URL.Path))\n}\n"
  },
  {
    "path": "interfaces/rest/users.go",
    "content": "package rest\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\n\t\"github.com/gorilla/mux\"\n\t\"github.com/spy16/droplets/domain\"\n\t\"github.com/spy16/droplets/pkg/logger\"\n)\n\nfunc addUsersAPI(router *mux.Router, reg registration, ret retriever, logger logger.Logger) {\n\tuc := &userController{\n\t\tLogger: logger,\n\t\treg:    reg,\n\t\tret:    ret,\n\t}\n\n\trouter.HandleFunc(\"/v1/users/{name}\", uc.get).Methods(http.MethodGet)\n\trouter.HandleFunc(\"/v1/users/\", uc.search).Methods(http.MethodGet)\n\trouter.HandleFunc(\"/v1/users/\", uc.post).Methods(http.MethodPost)\n}\n\ntype userController struct {\n\tlogger.Logger\n\treg registration\n\tret retriever\n}\n\nfunc (uc *userController) get(wr http.ResponseWriter, req *http.Request) {\n\tvars := mux.Vars(req)\n\tuser, err := uc.ret.Get(req.Context(), vars[\"name\"])\n\tif err != nil {\n\t\trespondErr(wr, err)\n\t\treturn\n\t}\n\n\trespond(wr, http.StatusOK, user)\n}\n\nfunc (uc *userController) search(wr http.ResponseWriter, req *http.Request) {\n\tvals := req.URL.Query()[\"t\"]\n\tusers, err := uc.ret.Search(req.Context(), vals, 10)\n\tif err != nil {\n\t\trespondErr(wr, err)\n\t\treturn\n\t}\n\n\trespond(wr, http.StatusOK, users)\n}\n\nfunc (uc *userController) post(wr http.ResponseWriter, req *http.Request) {\n\tuser := domain.User{}\n\tif err := readRequest(req, &user); err != nil {\n\t\tuc.Warnf(\"failed to read user request: %s\", err)\n\t\trespond(wr, http.StatusBadRequest, err)\n\t\treturn\n\t}\n\n\tregistered, err := uc.reg.Register(req.Context(), user)\n\tif err != nil {\n\t\tuc.Warnf(\"failed to register user: %s\", err)\n\t\trespondErr(wr, err)\n\t\treturn\n\t}\n\n\tuc.Infof(\"new user registered with id '%s'\", registered.Name)\n\trespond(wr, http.StatusCreated, registered)\n}\n\ntype registration interface {\n\tRegister(ctx context.Context, user domain.User) (*domain.User, error)\n}\n\ntype retriever interface {\n\tGet(ctx context.Context, name string) (*domain.User, error)\n\tSearch(ctx context.Context, tags []string, limit int) ([]domain.User, error)\n\tVerifySecret(ctx context.Context, name, secret string) bool\n}\n"
  },
  {
    "path": "interfaces/rest/utils.go",
    "content": "package rest\n\nimport (\n\t\"encoding/json\"\n\t\"net/http\"\n\n\t\"github.com/spy16/droplets/pkg/errors\"\n\t\"github.com/spy16/droplets/pkg/render\"\n)\n\nfunc respond(wr http.ResponseWriter, status int, v interface{}) {\n\tif err := render.JSON(wr, status, v); err != nil {\n\t\tif loggable, ok := wr.(errorLogger); ok {\n\t\t\tloggable.Errorf(\"failed to write data to http ResponseWriter: %s\", err)\n\t\t}\n\t}\n}\n\nfunc respondErr(wr http.ResponseWriter, err error) {\n\tif e, ok := err.(*errors.Error); ok {\n\t\trespond(wr, e.Code, e)\n\t\treturn\n\t}\n\trespond(wr, http.StatusInternalServerError, err)\n}\n\nfunc readRequest(req *http.Request, v interface{}) error {\n\tif err := json.NewDecoder(req.Body).Decode(v); err != nil {\n\t\treturn errors.Validation(\"Failed to read request body\")\n\t}\n\n\treturn nil\n}\n\ntype errorLogger interface {\n\tErrorf(msg string, args ...interface{})\n}\n"
  },
  {
    "path": "interfaces/web/app.go",
    "content": "package web\n\nimport (\n\t\"html/template\"\n\t\"net/http\"\n)\n\ntype app struct {\n\trender func(wr http.ResponseWriter, tpl string, data interface{})\n\ttpl    template.Template\n}\n\nfunc (app app) indexHandler(wr http.ResponseWriter, req *http.Request) {\n\tapp.render(wr, \"index.tpl\", nil)\n}\n"
  },
  {
    "path": "interfaces/web/doc.go",
    "content": "// Package web contains MVC style web app.\npackage web\n"
  },
  {
    "path": "interfaces/web/fs.go",
    "content": "package web\n\nimport (\n\t\"net/http\"\n\t\"os\"\n\n\t\"github.com/spy16/droplets/pkg/logger\"\n)\n\nfunc newSafeFileSystemServer(lg logger.Logger, root string) http.Handler {\n\tsfs := &safeFileSystem{\n\t\tfs:     http.Dir(root),\n\t\tLogger: lg,\n\t}\n\treturn http.FileServer(sfs)\n}\n\n// safeFileSystem implements http.FileSystem. It is used to prevent directory\n// listing of static assets.\ntype safeFileSystem struct {\n\tlogger.Logger\n\n\tfs http.FileSystem\n}\n\nfunc (sfs safeFileSystem) Open(path string) (http.File, error) {\n\tf, err := sfs.fs.Open(path)\n\tif err != nil {\n\t\tsfs.Warnf(\"failed to open file '%s': %v\", path, err)\n\t\treturn nil, err\n\t}\n\n\tstat, err := f.Stat()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif stat.IsDir() {\n\t\tsfs.Warnf(\"path '%s' is a directory, rejecting static path request\", path)\n\t\treturn nil, os.ErrNotExist\n\t}\n\n\treturn f, nil\n}\n"
  },
  {
    "path": "interfaces/web/web.go",
    "content": "package web\n\nimport (\n\t\"html/template\"\n\t\"io/ioutil\"\n\t\"net/http\"\n\t\"path/filepath\"\n\n\t\"github.com/gorilla/mux\"\n\t\"github.com/spy16/droplets/pkg/logger\"\n)\n\n// New initializes a new webapp server.\nfunc New(lg logger.Logger, cfg Config) (http.Handler, error) {\n\ttpl, err := initTemplate(lg, \"\", cfg.TemplateDir)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tapp := &app{\n\t\trender: func(wr http.ResponseWriter, tplName string, data interface{}) {\n\t\t\tif err := tpl.ExecuteTemplate(wr, tplName, data); err != nil {\n\t\t\t\tlg.Errorf(\"failed to render template '%s': %+v\", tplName, err)\n\t\t\t}\n\t\t},\n\t}\n\n\tfsServer := newSafeFileSystemServer(lg, cfg.StaticDir)\n\n\trouter := mux.NewRouter()\n\trouter.PathPrefix(\"/static\").Handler(http.StripPrefix(\"/static\", fsServer))\n\trouter.Handle(\"/favicon.ico\", fsServer)\n\n\t// web app routes\n\trouter.HandleFunc(\"/\", app.indexHandler)\n\n\treturn router, nil\n}\n\n// Config represents server configuration.\ntype Config struct {\n\tTemplateDir string\n\tStaticDir   string\n}\n\nfunc initTemplate(lg logger.Logger, name, path string) (*template.Template, error) {\n\tapath, err := filepath.Abs(path)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tfiles, err := ioutil.ReadDir(apath)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tlg.Infof(\"loading templates from '%s'...\", path)\n\ttpl := template.New(name)\n\tfor _, f := range files {\n\t\tif f.IsDir() {\n\t\t\tcontinue\n\t\t}\n\t\tfp := filepath.Join(apath, f.Name())\n\t\tlg.Debugf(\"parsing template file '%s'\", f.Name())\n\t\ttpl.New(f.Name()).ParseFiles(fp)\n\t}\n\n\treturn tpl, nil\n}\n"
  },
  {
    "path": "main.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\t\"os\"\n\t\"time\"\n\n\t\"github.com/gorilla/mux\"\n\t\"github.com/spf13/viper\"\n\t\"github.com/spy16/droplets/interfaces/mongo\"\n\t\"github.com/spy16/droplets/interfaces/rest\"\n\t\"github.com/spy16/droplets/interfaces/web\"\n\t\"github.com/spy16/droplets/pkg/graceful\"\n\t\"github.com/spy16/droplets/pkg/logger\"\n\t\"github.com/spy16/droplets/pkg/middlewares\"\n\t\"github.com/spy16/droplets/usecases/posts\"\n\t\"github.com/spy16/droplets/usecases/users\"\n)\n\nfunc main() {\n\tcfg := loadConfig()\n\tlg := logger.New(os.Stderr, cfg.LogLevel, cfg.LogFormat)\n\n\tdb, closeSession, err := mongo.Connect(cfg.MongoURI, true)\n\tif err != nil {\n\t\tlg.Fatalf(\"failed to connect to mongodb: %v\", err)\n\t}\n\tdefer closeSession()\n\n\tlg.Debugf(\"setting up rest api service\")\n\tuserStore := mongo.NewUserStore(db)\n\tpostStore := mongo.NewPostStore(db)\n\n\tuserRegistration := users.NewRegistrar(lg, userStore)\n\tuserRetriever := users.NewRetriever(lg, userStore)\n\n\tpostPub := posts.NewPublication(lg, postStore, userStore)\n\tpostRet := posts.NewRetriever(lg, postStore)\n\n\trestHandler := rest.New(lg, userRegistration, userRetriever, postRet, postPub)\n\twebHandler, err := web.New(lg, web.Config{\n\t\tTemplateDir: cfg.TemplateDir,\n\t\tStaticDir:   cfg.StaticDir,\n\t})\n\tif err != nil {\n\t\tlg.Fatalf(\"failed to setup web handler: %v\", err)\n\t}\n\n\tsrv := setupServer(cfg, lg, webHandler, restHandler)\n\tlg.Infof(\"listening for requests on :8080...\")\n\tif err := srv.ListenAndServe(); err != nil {\n\t\tlg.Fatalf(\"http server exited: %s\", err)\n\t}\n}\n\nfunc setupServer(cfg config, lg logger.Logger, web http.Handler, rest http.Handler) *graceful.Server {\n\trest = middlewares.WithBasicAuth(lg, rest,\n\t\tmiddlewares.UserVerifierFunc(func(ctx context.Context, name, secret string) bool {\n\t\t\treturn secret == \"secret@123\"\n\t\t}),\n\t)\n\n\trouter := mux.NewRouter()\n\trouter.PathPrefix(\"/api\").Handler(http.StripPrefix(\"/api\", rest))\n\trouter.PathPrefix(\"/\").Handler(web)\n\n\thandler := middlewares.WithRequestLogging(lg, router)\n\thandler = middlewares.WithRecovery(lg, handler)\n\n\tsrv := graceful.NewServer(handler, cfg.GracefulTimeout, os.Interrupt)\n\tsrv.Log = lg.Errorf\n\tsrv.Addr = cfg.Addr\n\treturn srv\n}\n\ntype config struct {\n\tAddr            string\n\tLogLevel        string\n\tLogFormat       string\n\tStaticDir       string\n\tTemplateDir     string\n\tGracefulTimeout time.Duration\n\tMongoURI        string\n}\n\nfunc loadConfig() config {\n\tviper.SetDefault(\"MONGO_URI\", \"mongodb://localhost/droplets\")\n\tviper.SetDefault(\"LOG_LEVEL\", \"debug\")\n\tviper.SetDefault(\"LOG_FORMAT\", \"text\")\n\tviper.SetDefault(\"ADDR\", \":8080\")\n\tviper.SetDefault(\"STATIC_DIR\", \"./web/static/\")\n\tviper.SetDefault(\"TEMPLATE_DIR\", \"./web/templates/\")\n\tviper.SetDefault(\"GRACEFUL_TIMEOUT\", 20*time.Second)\n\n\tviper.ReadInConfig()\n\tviper.AutomaticEnv()\n\n\treturn config{\n\t\t// application configuration\n\t\tAddr:            viper.GetString(\"ADDR\"),\n\t\tStaticDir:       viper.GetString(\"STATIC_DIR\"),\n\t\tTemplateDir:     viper.GetString(\"TEMPLATE_DIR\"),\n\t\tLogLevel:        viper.GetString(\"LOG_LEVEL\"),\n\t\tLogFormat:       viper.GetString(\"LOG_FORMAT\"),\n\t\tGracefulTimeout: viper.GetDuration(\"GRACEFUL_TIMEOUT\"),\n\n\t\t// store configuration\n\t\tMongoURI: viper.GetString(\"MONGO_URI\"),\n\t}\n}\n"
  },
  {
    "path": "pkg/doc.go",
    "content": "// Package pkg is the root for re-usable packages. This package should not\n// contain any entities (exported or otherwise) since, a package named `pkg`\n// does not express anything about its purpose and could become a catch all\n// package like `utils`, `misc` etc. which should be avoided.\npackage pkg\n"
  },
  {
    "path": "pkg/errors/authorization.go",
    "content": "package errors\n\nimport \"net/http\"\n\n// Common authorization related errors\nconst (\n\tTypeUnauthorized = \"Unauthorized\"\n)\n\n// Unauthorized can be used to generate an error that represents an unauthorized\n// request.\nfunc Unauthorized(reason string) error {\n\treturn WithStack(&Error{\n\t\tCode:    http.StatusUnauthorized,\n\t\tType:    TypeUnauthorized,\n\t\tMessage: \"You are not authorized to perform the requested action\",\n\t\tContext: map[string]interface{}{\n\t\t\t\"reason\": reason,\n\t\t},\n\t})\n}\n"
  },
  {
    "path": "pkg/errors/doc.go",
    "content": "// Package errors provides common error definitions and tools for dealing\n// with errors. The API of this package is drop-in replacement for standard\n// errors package except for one significant difference:\n//   Since Error type used in this package embeds map inside, errors created\n// by this package are not comparable and  hence cannot  be used to create\n// Sentinel Errors.\npackage errors\n"
  },
  {
    "path": "pkg/errors/error.go",
    "content": "package errors\n\nimport (\n\t\"fmt\"\n\t\"io\"\n)\n\n// Error is a generic error representation with some fields to provide additional\n// context around the error.\ntype Error struct {\n\t// Code can represent an http error code.\n\tCode int `json:\"-\"`\n\n\t// Type should be an error code to identify the error. Type and Context together\n\t// should provide enough context for robust error handling on client side.\n\tType string `json:\"type,omitempty\"`\n\n\t// Context can contain additional information describing the error. Context will\n\t// be exposed only in API endpoints so that clients be integrated effectively.\n\tContext map[string]interface{} `json:\"context,omitempty\"`\n\n\t// Message should be a user-friendly error message which can be shown to the\n\t// end user without further modifications. However, clients are free to modify\n\t// this (e.g., for enabling localization), or augment this message with the\n\t// information available in the context before rendering a message to the end\n\t// user.\n\tMessage string `json:\"message,omitempty\"`\n\n\t// original can contain an underlying error if any. This value will be returned\n\t// by the Cause() method.\n\toriginal error\n\n\t// stack will contain a minimal stack trace which can be used for logging and\n\t// debugging. stack should not be examined to handle errors.\n\tstack stack\n}\n\n// Cause returns the underlying error if any.\nfunc (err Error) Cause() error {\n\treturn err.original\n}\n\nfunc (err Error) Error() string {\n\tif origin := err.Cause(); origin != nil {\n\t\treturn fmt.Sprintf(\"%s: %s: %s\", origin, err.Type, err.Message)\n\t}\n\n\treturn fmt.Sprintf(\"%s: %s\", err.Type, err.Message)\n}\n\n// Format implements fmt.Formatter interface.\nfunc (err Error) Format(st fmt.State, verb rune) {\n\tswitch verb {\n\tcase 'v':\n\t\tif st.Flag('+') {\n\t\t\tio.WriteString(st, err.Error())\n\t\t\terr.stack.Format(st, verb)\n\t\t} else {\n\t\t\tfmt.Fprintf(st, \"%s: \", err.Type)\n\t\t\tfor key, val := range err.Context {\n\t\t\t\tfmt.Fprintf(st, \"%s='%s' \", key, val)\n\t\t\t}\n\t\t}\n\tcase 's':\n\t\tio.WriteString(st, err.Error())\n\tcase 'q':\n\t\tfmt.Fprintf(st, \"%q\", err.Error())\n\t}\n}\n"
  },
  {
    "path": "pkg/errors/errors.go",
    "content": "package errors\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n)\n\n// TypeUnknown represents unknown error type.\nconst TypeUnknown = \"Unknown\"\n\n// New returns an error object with formatted error message generated using\n// the arguments.\nfunc New(msg string, args ...interface{}) error {\n\treturn &Error{\n\t\tCode:    http.StatusInternalServerError,\n\t\tType:    TypeUnknown,\n\t\tMessage: fmt.Sprintf(msg, args...),\n\t\tstack:   callStack(3),\n\t}\n}\n\n// Type attempts converting the err to Error type and extracts error Type.\n// If conversion not possible, returns TypeUnknown.\nfunc Type(err error) string {\n\tif e, ok := err.(*Error); ok {\n\t\treturn e.Type\n\t}\n\treturn TypeUnknown\n}\n\n// Wrapf wraps the given err with formatted message and returns a new error.\nfunc Wrapf(err error, msg string, args ...interface{}) error {\n\treturn WithStack(&Error{\n\t\tCode:     http.StatusInternalServerError,\n\t\tType:     TypeUnknown,\n\t\tMessage:  fmt.Sprintf(msg, args...),\n\t\toriginal: err,\n\t})\n}\n\n// WithStack annotates the given error with stack trace and returns the wrapped\n// error.\nfunc WithStack(err error) error {\n\tvar wrappedErr Error\n\tif e, ok := err.(*Error); ok {\n\t\twrappedErr = *e\n\t} else {\n\t\twrappedErr.Type = TypeUnknown\n\t\twrappedErr.Message = \"Something went wrong\"\n\t\twrappedErr.original = err\n\t}\n\n\twrappedErr.stack = callStack(3)\n\treturn &wrappedErr\n}\n\n// Cause returns the underlying error if the given error is wrapping another error.\nfunc Cause(err error) error {\n\tif err == nil {\n\t\treturn nil\n\t}\n\n\tif e, ok := err.(*Error); ok {\n\t\treturn e\n\t}\n\n\treturn err\n}\n"
  },
  {
    "path": "pkg/errors/resource.go",
    "content": "package errors\n\nimport \"net/http\"\n\n// Common resource related error codes.\nconst (\n\tTypeResourceNotFound = \"ResourceNotFound\"\n\tTypeResourceConflict = \"ResourceConflict\"\n)\n\n// ResourceNotFound returns an error that represents an attempt to access a\n// non-existent resource.\nfunc ResourceNotFound(rType, rID string) error {\n\treturn WithStack(&Error{\n\t\tCode:    http.StatusNotFound,\n\t\tType:    TypeResourceNotFound,\n\t\tMessage: \"Resource you are requesting does not exist\",\n\t\tContext: map[string]interface{}{\n\t\t\t\"resource_type\": rType,\n\t\t\t\"resource_id\":   rID,\n\t\t},\n\t})\n}\n\n// Conflict returns an error that represents a resource identifier conflict.\nfunc Conflict(rType, rID string) error {\n\treturn WithStack(&Error{\n\t\tCode:    http.StatusConflict,\n\t\tType:    TypeResourceConflict,\n\t\tMessage: \"A resource with same name already exists\",\n\t\tContext: map[string]interface{}{\n\t\t\t\"resource_type\": rType,\n\t\t\t\"resource_id\":   rID,\n\t\t},\n\t})\n}\n"
  },
  {
    "path": "pkg/errors/stack.go",
    "content": "package errors\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"path\"\n\t\"runtime\"\n)\n\nconst depth = 32\n\ntype stack []frame\n\n// Format formats the stack of Frames according to the fmt.Formatter interface.\n//\n//    %s\tlists source files for each Frame in the stack\n//    %v\tlists the source file and line number for each Frame in the stack\n//\n// Format accepts flags that alter the printing of some verbs, as follows:\n//\n//    %+v   Prints filename, function, and line number for each Frame in the stack.\nfunc (st stack) Format(s fmt.State, verb rune) {\n\tswitch verb {\n\tcase 'v':\n\t\tswitch {\n\t\tcase s.Flag('+'):\n\t\t\tfor _, f := range st {\n\t\t\t\tfmt.Fprintf(s, \"\\n%+v\", f)\n\t\t\t}\n\t\tcase s.Flag('#'):\n\t\t\tfmt.Fprintf(s, \"%#v\", []frame(st))\n\t\tdefault:\n\t\t\tfmt.Fprintf(s, \"%v\", []frame(st))\n\t\t}\n\tcase 's':\n\t\tfmt.Fprintf(s, \"%s\", []frame(st))\n\t}\n}\n\ntype frame struct {\n\tfn   string\n\tfile string\n\tline int\n}\n\n// Format formats the frame according to the fmt.Formatter interface.\n//\n//    %s    source file\n//    %d    source line\n//    %n    function name\n//    %v    equivalent to %s:%d\n//\n// Format accepts flags that alter the printing of some verbs, as follows:\n//\n//    %+s   function name and path of source file relative to the compile time\n//          GOPATH separated by \\n\\t (<funcname>\\n\\t<path>)\n//    %+v   equivalent to %+s:%d\nfunc (f frame) Format(s fmt.State, verb rune) {\n\tswitch verb {\n\tcase 's':\n\t\tswitch {\n\t\tcase s.Flag('+'):\n\t\t\tfmt.Fprintf(s, \"%s\\n\\t%s\", f.fn, f.file)\n\t\tdefault:\n\t\t\tio.WriteString(s, path.Base(f.file))\n\t\t}\n\tcase 'd':\n\t\tfmt.Fprintf(s, \"%d\", f.line)\n\tcase 'n':\n\t\tio.WriteString(s, f.fn)\n\tcase 'v':\n\t\tf.Format(s, 's')\n\t\tio.WriteString(s, \":\")\n\t\tf.Format(s, 'd')\n\t}\n}\n\nfunc callStack(skip int) stack {\n\tvar frames []frame\n\tvar pcs [depth]uintptr\n\n\tn := runtime.Callers(skip, pcs[:])\n\tfor i := 0; i < n; i++ {\n\t\tpc := pcs[i]\n\t\tframe := frameFromPC(pc)\n\t\tframes = append(frames, frame)\n\t}\n\treturn stack(frames)\n}\n\nfunc frameFromPC(pc uintptr) frame {\n\tfr := frame{}\n\n\tfn := runtime.FuncForPC(pc)\n\tif fn == nil {\n\t\tfr.fn = \"unknown\"\n\t} else {\n\t\tfr.fn = fn.Name()\n\t}\n\n\tfr.file, fr.line = fn.FileLine(pc)\n\treturn fr\n}\n"
  },
  {
    "path": "pkg/errors/validation.go",
    "content": "package errors\n\nimport \"net/http\"\n\n// Common validation error type codes.\nconst (\n\tTypeInvalidRequest = \"InvalidRequest\"\n\tTypeMissingField   = \"MissingField\"\n\tTypeInvalidValue   = \"InvalidValue\"\n)\n\n// Validation returns an error that can be used to represent an invalid request.\nfunc Validation(reason string) error {\n\treturn WithStack(&Error{\n\t\tCode:    http.StatusBadRequest,\n\t\tType:    TypeInvalidRequest,\n\t\tMessage: reason,\n\t\tContext: map[string]interface{}{},\n\t})\n}\n\n// InvalidValue can be used to generate an error that represents an invalid\n// value for the 'field'. reason should be used to add detail describing why\n// the value is invalid.\nfunc InvalidValue(field string, reason string) error {\n\treturn WithStack(&Error{\n\t\tCode:    http.StatusBadRequest,\n\t\tType:    TypeInvalidValue,\n\t\tMessage: \"A parameter has invalid value\",\n\t\tContext: map[string]interface{}{\n\t\t\t\"field\":  field,\n\t\t\t\"reason\": reason,\n\t\t},\n\t})\n}\n\n// MissingField can be used to generate an error that represents\n// a empty value for a required field.\nfunc MissingField(field string) error {\n\treturn WithStack(&Error{\n\t\tCode:    http.StatusBadRequest,\n\t\tType:    TypeMissingField,\n\t\tMessage: \"A required field is missing\",\n\t\tContext: map[string]interface{}{\n\t\t\t\"field\": field,\n\t\t},\n\t})\n}\n"
  },
  {
    "path": "pkg/graceful/doc.go",
    "content": "// Package graceful provides a simple wrapper for http.Handler which\n// handles graceful shutdown based on registered signals. Server in\n// this package closely follows the http.Server struct but can not be\n// used as a drop-in replacement.\npackage graceful\n"
  },
  {
    "path": "pkg/graceful/graceful.go",
    "content": "package graceful\n\nimport (\n\t\"context\"\n\t\"log\"\n\t\"net\"\n\t\"net/http\"\n\t\"os\"\n\t\"os/signal\"\n\t\"time\"\n)\n\n// LogFunc can be set on the server to customize the message printed when the\n// server is shutting down.\ntype LogFunc func(msg string, args ...interface{})\n\n// NewServer creates a wrapper around the given handler.\nfunc NewServer(handler http.Handler, timeout time.Duration, signals ...os.Signal) *Server {\n\tgss := &Server{}\n\tgss.server = &http.Server{Handler: handler}\n\tgss.signals = signals\n\tgss.Log = log.Printf\n\tgss.timeout = timeout\n\treturn gss\n}\n\n// Server is a wrapper around an http handler. It provides methods\n// to start the server with graceful-shutdown enabled.\ntype Server struct {\n\tAddr string\n\tLog  LogFunc\n\n\tserver   *http.Server\n\tsignals  []os.Signal\n\ttimeout  time.Duration\n\tstartErr error\n}\n\n// Serve starts the http listener with the registered http.Handler and\n// then blocks until a interrupt signal is received.\nfunc (gss *Server) Serve(l net.Listener) error {\n\tctx, cancel := context.WithCancel(context.Background())\n\tgo func() {\n\t\tif err := gss.server.Serve(l); err != nil {\n\t\t\tgss.startErr = err\n\t\t\tcancel()\n\t\t}\n\t}()\n\treturn gss.waitForInterrupt(ctx)\n}\n\n// ServeTLS starts the http listener with the registered http.Handler and\n// then blocks until a interrupt signal is received.\nfunc (gss *Server) ServeTLS(l net.Listener, certFile, keyFile string) error {\n\tctx, cancel := context.WithCancel(context.Background())\n\tgo func() {\n\t\tif err := gss.server.ServeTLS(l, certFile, keyFile); err != nil {\n\t\t\tgss.startErr = err\n\t\t\tcancel()\n\t\t}\n\t}()\n\treturn gss.waitForInterrupt(ctx)\n}\n\n// ListenAndServe serves the requests on a listener bound to interface\n// specified by Addr\nfunc (gss *Server) ListenAndServe() error {\n\tctx, cancel := context.WithCancel(context.Background())\n\tgo func() {\n\t\tgss.server.Addr = gss.Addr\n\t\tif err := gss.server.ListenAndServe(); err != http.ErrServerClosed {\n\t\t\tgss.startErr = err\n\t\t\tcancel()\n\t\t}\n\t}()\n\treturn gss.waitForInterrupt(ctx)\n}\n\n// ListenAndServeTLS serves the requests on a listener bound to interface\n// specified by Addr\nfunc (gss *Server) ListenAndServeTLS(certFile, keyFile string) error {\n\tctx, cancel := context.WithCancel(context.Background())\n\tgo func() {\n\t\tif err := gss.server.ListenAndServeTLS(certFile, keyFile); err != http.ErrServerClosed {\n\t\t\tgss.startErr = err\n\t\t\tcancel()\n\t\t}\n\t}()\n\treturn gss.waitForInterrupt(ctx)\n}\n\nfunc (gss *Server) waitForInterrupt(ctx context.Context) error {\n\tsigCh := make(chan os.Signal, 1)\n\tsignal.Notify(sigCh, gss.signals...)\n\n\tselect {\n\tcase sig := <-sigCh:\n\t\tif gss.Log != nil {\n\t\t\tgss.Log(\"shutting down (signal=%s)...\", sig)\n\t\t}\n\t\tbreak\n\n\tcase <-ctx.Done():\n\t\treturn gss.startErr\n\t}\n\n\treturn gss.shutdown()\n}\n\nfunc (gss *Server) shutdown() error {\n\tctx, cancel := context.WithTimeout(context.Background(), gss.timeout)\n\tdefer cancel()\n\treturn gss.server.Shutdown(ctx)\n}\n"
  },
  {
    "path": "pkg/logger/doc.go",
    "content": "// Package logger provides logging functions. The loggers implemented in this\n// package will have the API defined by the Logger interface. The interface\n// is defined here (instead of where it is being used which is the right place),\n// is because Logger interface is a common thing that gets used across the code\n// base while being fairly constant in terms of its API.\npackage logger\n\n// Logger implementation is responsible for providing structured and levled\n// logging functions.\ntype Logger interface {\n\tDebugf(msg string, args ...interface{})\n\tInfof(msg string, args ...interface{})\n\tWarnf(msg string, args ...interface{})\n\tErrorf(msg string, args ...interface{})\n\tFatalf(msg string, args ...interface{})\n\n\t// WithFields should return a logger which is annotated with the given\n\t// fields. These fields should be added to every logging call on the\n\t// returned logger.\n\tWithFields(m map[string]interface{}) Logger\n}\n"
  },
  {
    "path": "pkg/logger/logrus.go",
    "content": "package logger\n\nimport (\n\t\"io\"\n\t\"os\"\n\n\t\"github.com/sirupsen/logrus\"\n)\n\n// New returns a logger implemented using the logrus package.\nfunc New(wr io.Writer, level string, format string) Logger {\n\tif wr == nil {\n\t\twr = os.Stderr\n\t}\n\n\tlr := logrus.New()\n\tlr.SetOutput(wr)\n\tlr.SetFormatter(&logrus.TextFormatter{})\n\tif format == \"json\" {\n\t\tlr.SetFormatter(&logrus.JSONFormatter{})\n\t}\n\n\tlvl, err := logrus.ParseLevel(level)\n\tif err != nil {\n\t\tlvl = logrus.WarnLevel\n\t\tlr.Warnf(\"failed to parse log-level '%s', defaulting to 'warning'\", level)\n\t}\n\tlr.SetLevel(lvl)\n\n\treturn &logrusLogger{\n\t\tEntry: logrus.NewEntry(lr),\n\t}\n}\n\n// logrusLogger provides functions for structured logging.\ntype logrusLogger struct {\n\t*logrus.Entry\n}\n\nfunc (ll *logrusLogger) WithFields(fields map[string]interface{}) Logger {\n\tannotatedEntry := ll.Entry.WithFields(logrus.Fields(fields))\n\treturn &logrusLogger{\n\t\tEntry: annotatedEntry,\n\t}\n}\n"
  },
  {
    "path": "pkg/middlewares/authn.go",
    "content": "package middlewares\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\n\t\"github.com/spy16/droplets/pkg/errors\"\n\t\"github.com/spy16/droplets/pkg/logger\"\n\t\"github.com/spy16/droplets/pkg/render\"\n)\n\nvar authUser = ctxKey(\"user\")\n\n// WithBasicAuth adds Basic authentication checks to the handler. Basic Auth header\n// will be extracted from the request and verified using the verifier.\nfunc WithBasicAuth(lg logger.Logger, next http.Handler, verifier UserVerifier) http.Handler {\n\treturn http.HandlerFunc(func(wr http.ResponseWriter, req *http.Request) {\n\t\tname, secret, ok := req.BasicAuth()\n\t\tif !ok {\n\t\t\trender.JSON(wr, http.StatusUnauthorized, errors.Unauthorized(\"Basic auth header is not present\"))\n\t\t\treturn\n\t\t}\n\n\t\tverified := verifier.VerifySecret(req.Context(), name, secret)\n\t\tif !verified {\n\t\t\twr.WriteHeader(http.StatusUnauthorized)\n\t\t\trender.JSON(wr, http.StatusUnauthorized, errors.Unauthorized(\"Invalid username or secret\"))\n\t\t\treturn\n\t\t}\n\n\t\treq = req.WithContext(context.WithValue(req.Context(), authUser, name))\n\t\tnext.ServeHTTP(wr, req)\n\t})\n}\n\n// User extracts the username injected into the context by the auth middleware.\nfunc User(req *http.Request) (string, bool) {\n\tval := req.Context().Value(authUser)\n\tif userName, ok := val.(string); ok {\n\t\treturn userName, true\n\t}\n\n\treturn \"\", false\n}\n\ntype ctxKey string\n\n// UserVerifier implementation is responsible for verifying the name-secret pair.\ntype UserVerifier interface {\n\tVerifySecret(ctx context.Context, name, secret string) bool\n}\n\n// UserVerifierFunc implements UserVerifier.\ntype UserVerifierFunc func(ctx context.Context, name, secret string) bool\n\n// VerifySecret delegates call to the wrapped function.\nfunc (uvf UserVerifierFunc) VerifySecret(ctx context.Context, name, secret string) bool {\n\treturn uvf(ctx, name, secret)\n}\n"
  },
  {
    "path": "pkg/middlewares/doc.go",
    "content": "// Package middlewares contains re-usable middleware functions. Middleware\n// functions in this package follow standard http.HandlerFunc signature and\n// hence are compatible with all standard http library functions.\npackage middlewares\n"
  },
  {
    "path": "pkg/middlewares/logging.go",
    "content": "package middlewares\n\nimport (\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/spy16/droplets/pkg/logger\"\n)\n\n// WithRequestLogging adds logging to the given handler. Every request handled by\n// 'next' will be logged with request information such as path, method, latency,\n// client-ip, response status code etc. Logging will be done at info level only.\n// Also, injects a logger into the ResponseWriter which can be later used by the\n// handlers to perform additional logging.\nfunc WithRequestLogging(logger logger.Logger, next http.Handler) http.Handler {\n\treturn http.HandlerFunc(func(wr http.ResponseWriter, req *http.Request) {\n\t\twrappedWr := wrap(wr, logger)\n\n\t\tstart := time.Now()\n\t\tdefer logRequest(logger, start, wrappedWr, req)\n\n\t\tnext.ServeHTTP(wrappedWr, req)\n\n\t})\n}\n\nfunc logRequest(logger logger.Logger, startedAt time.Time, wr *wrappedWriter, req *http.Request) {\n\tduration := time.Now().Sub(startedAt)\n\n\tinfo := map[string]interface{}{\n\t\t\"latency\": duration,\n\t\t\"status\":  wr.wroteStatus,\n\t}\n\n\tlogger.\n\t\tWithFields(requestInfo(req)).\n\t\tWithFields(info).\n\t\tInfof(\"request completed with code %d\", wr.wroteStatus)\n}\n"
  },
  {
    "path": "pkg/middlewares/recovery.go",
    "content": "package middlewares\n\nimport (\n\t\"encoding/json\"\n\t\"net/http\"\n\n\t\"github.com/spy16/droplets/pkg/logger\"\n)\n\n// WithRecovery recovers from any panics and logs them appropriately.\nfunc WithRecovery(logger logger.Logger, next http.Handler) http.Handler {\n\treturn http.HandlerFunc(func(wr http.ResponseWriter, req *http.Request) {\n\t\tri := recoveryInfo{}\n\t\tsafeHandler(next, &ri).ServeHTTP(wr, req)\n\n\t\tif ri.panicked {\n\t\t\tlogger.Errorf(\"recovered from panic: %+v\", ri.val)\n\n\t\t\twr.WriteHeader(http.StatusInternalServerError)\n\t\t\tjson.NewEncoder(wr).Encode(map[string]interface{}{\n\t\t\t\t\"error\": \"Something went wrong\",\n\t\t\t})\n\t\t}\n\t})\n}\n\nfunc safeHandler(next http.Handler, ri *recoveryInfo) http.Handler {\n\treturn http.HandlerFunc(func(wr http.ResponseWriter, req *http.Request) {\n\t\tdefer func() {\n\t\t\tif val := recover(); val != nil {\n\t\t\t\tri.panicked = true\n\t\t\t\tri.val = val\n\t\t\t}\n\t\t}()\n\n\t\tnext.ServeHTTP(wr, req)\n\t})\n}\n\ntype recoveryInfo struct {\n\tpanicked bool\n\tval      interface{}\n}\n"
  },
  {
    "path": "pkg/middlewares/utils.go",
    "content": "package middlewares\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/spy16/droplets/pkg/logger\"\n)\n\nfunc requestInfo(req *http.Request) map[string]interface{} {\n\treturn map[string]interface{}{\n\t\t\"path\":   req.URL.Path,\n\t\t\"query\":  req.URL.RawQuery,\n\t\t\"method\": req.Method,\n\t\t\"client\": req.RemoteAddr,\n\t}\n}\n\nfunc wrap(wr http.ResponseWriter, logger logger.Logger) *wrappedWriter {\n\treturn &wrappedWriter{\n\t\tResponseWriter: wr,\n\t\tLogger:         logger,\n\t\twroteStatus:    http.StatusOK,\n\t}\n}\n\ntype wrappedWriter struct {\n\thttp.ResponseWriter\n\tlogger.Logger\n\n\twroteStatus int\n}\n\nfunc (wr *wrappedWriter) WriteHeader(statusCode int) {\n\twr.wroteStatus = statusCode\n\twr.ResponseWriter.WriteHeader(statusCode)\n}\n"
  },
  {
    "path": "pkg/render/doc.go",
    "content": "// Package render provides simple and generic functions for rendering\n// data structures using different encoding formats.\npackage render\n"
  },
  {
    "path": "pkg/render/render.go",
    "content": "package render\n\nimport (\n\t\"encoding/json\"\n\t\"io\"\n\t\"net/http\"\n)\n\nconst contentTypeJSON = \"application/json; charset=utf-8\"\n\n// JSON encodes the given val using the standard json package and writes\n// the encoding output to the given writer. If the writer implements the\n// http.ResponseWriter interface, then this function will also set the\n// proper JSON content-type header with charset as UTF-8. Status will be\n// considered only when wr is http.ResponseWriter and in that case, status\n// must be a valid status code.\nfunc JSON(wr io.Writer, status int, val interface{}) error {\n\tif hw, ok := wr.(http.ResponseWriter); ok {\n\t\thw.Header().Set(\"Content-type\", contentTypeJSON)\n\t\thw.WriteHeader(status)\n\t}\n\n\treturn json.NewEncoder(wr).Encode(val)\n}\n"
  },
  {
    "path": "usecases/posts/doc.go",
    "content": "// Package posts has usecases around Post domain entity. This includes\n// publishing and management of posts.\npackage posts\n"
  },
  {
    "path": "usecases/posts/publish.go",
    "content": "package posts\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/spy16/droplets/domain\"\n\t\"github.com/spy16/droplets/pkg/errors\"\n\t\"github.com/spy16/droplets/pkg/logger\"\n)\n\n// NewPublication initializes the publication usecase.\nfunc NewPublication(lg logger.Logger, store Store, verifier userVerifier) *Publication {\n\treturn &Publication{\n\t\tLogger:   lg,\n\t\tstore:    store,\n\t\tverifier: verifier,\n\t}\n}\n\n// Publication implements the publishing usecases.\ntype Publication struct {\n\tlogger.Logger\n\n\tstore    Store\n\tverifier userVerifier\n}\n\n// Publish validates and persists the post into the store.\nfunc (pub *Publication) Publish(ctx context.Context, post domain.Post) (*domain.Post, error) {\n\tif err := post.Validate(); err != nil {\n\t\treturn nil, err\n\t}\n\n\tif !pub.verifier.Exists(ctx, post.Owner) {\n\t\treturn nil, errors.Unauthorized(fmt.Sprintf(\"user '%s' not found\", post.Owner))\n\t}\n\n\tif pub.store.Exists(ctx, post.Name) {\n\t\treturn nil, errors.Conflict(\"Post\", post.Name)\n\t}\n\n\tsaved, err := pub.store.Save(ctx, post)\n\tif err != nil {\n\t\tpub.Warnf(\"failed to save post to the store: %+v\", err)\n\t}\n\n\treturn saved, nil\n}\n\n// Delete removes the post from the store.\nfunc (pub *Publication) Delete(ctx context.Context, name string) (*domain.Post, error) {\n\treturn pub.store.Delete(ctx, name)\n}\n"
  },
  {
    "path": "usecases/posts/retrieval.go",
    "content": "package posts\n\nimport (\n\t\"context\"\n\t\"errors\"\n\n\t\"github.com/spy16/droplets/domain\"\n\t\"github.com/spy16/droplets/pkg/logger\"\n)\n\n// NewRetriever initializes the retrieval usecase with given store.\nfunc NewRetriever(lg logger.Logger, store Store) *Retriever {\n\treturn &Retriever{\n\t\tLogger: lg,\n\t\tstore:  store,\n\t}\n}\n\n// Retriever provides retrieval related usecases.\ntype Retriever struct {\n\tlogger.Logger\n\n\tstore Store\n}\n\n// Get finds a post by its name.\nfunc (ret *Retriever) Get(ctx context.Context, name string) (*domain.Post, error) {\n\treturn ret.store.Get(ctx, name)\n}\n\n// Search finds all the posts matching the parameters in the query.\nfunc (ret *Retriever) Search(ctx context.Context, query Query) ([]domain.Post, error) {\n\treturn nil, errors.New(\"not implemented\")\n}\n\n// Query represents parameters for executing a search. Zero valued fields\n// in the query will be ignored.\ntype Query struct {\n\tName  string   `json:\"name,omitempty\"`\n\tOwner string   `json:\"owner,omitempty\"`\n\tTags  []string `json:\"tags,omitempty\"`\n}\n"
  },
  {
    "path": "usecases/posts/store.go",
    "content": "package posts\n\nimport (\n\t\"context\"\n\n\t\"github.com/spy16/droplets/domain\"\n)\n\n// Store implementation is responsible for managing persistance of posts.\ntype Store interface {\n\tGet(ctx context.Context, name string) (*domain.Post, error)\n\tExists(ctx context.Context, name string) bool\n\tSave(ctx context.Context, post domain.Post) (*domain.Post, error)\n\tDelete(ctx context.Context, name string) (*domain.Post, error)\n}\n\n// userVerifier is responsible for verifying existence of a user.\ntype userVerifier interface {\n\tExists(ctx context.Context, name string) bool\n}\n"
  },
  {
    "path": "usecases/users/doc.go",
    "content": "// Package users has usecases around User domain entity. This includes\n// user registration, retrieval etc.\npackage users\n"
  },
  {
    "path": "usecases/users/registration.go",
    "content": "package users\n\nimport (\n\t\"context\"\n\n\t\"github.com/spy16/droplets/domain\"\n\t\"github.com/spy16/droplets/pkg/errors\"\n\t\"github.com/spy16/droplets/pkg/logger\"\n)\n\n// NewRegistrar initializes a Registration service object.\nfunc NewRegistrar(lg logger.Logger, store Store) *Registrar {\n\treturn &Registrar{\n\t\tLogger: lg,\n\t\tstore:  store,\n\t}\n}\n\n// Registrar provides functions for user registration.\ntype Registrar struct {\n\tlogger.Logger\n\n\tstore Store\n}\n\n// Register creates a new user in the system using the given user object.\nfunc (reg *Registrar) Register(ctx context.Context, user domain.User) (*domain.User, error) {\n\tif err := user.Validate(); err != nil {\n\t\treturn nil, err\n\t}\n\tif len(user.Secret) < 8 {\n\t\treturn nil, errors.InvalidValue(\"Secret\", \"secret must have 8 or more characters\")\n\t}\n\n\tif reg.store.Exists(ctx, user.Name) {\n\t\treturn nil, errors.Conflict(\"User\", user.Name)\n\t}\n\n\tif err := user.HashSecret(); err != nil {\n\t\treturn nil, err\n\t}\n\n\tsaved, err := reg.store.Save(ctx, user)\n\tif err != nil {\n\t\treg.Logger.Warnf(\"failed to save user object: %v\", err)\n\t\treturn nil, err\n\t}\n\n\tsaved.Secret = \"\"\n\treturn saved, nil\n}\n"
  },
  {
    "path": "usecases/users/retrieval.go",
    "content": "package users\n\nimport (\n\t\"context\"\n\n\t\"github.com/spy16/droplets/domain\"\n\t\"github.com/spy16/droplets/pkg/logger\"\n)\n\n// NewRetriever initializes an instance of Retriever with given store.\nfunc NewRetriever(lg logger.Logger, store Store) *Retriever {\n\treturn &Retriever{\n\t\tLogger: lg,\n\t\tstore:  store,\n\t}\n}\n\n// Retriever provides functions for retrieving user and user info.\ntype Retriever struct {\n\tlogger.Logger\n\n\tstore Store\n}\n\n// Search finds all users matching the tags.\nfunc (ret *Retriever) Search(ctx context.Context, tags []string, limit int) ([]domain.User, error) {\n\tusers, err := ret.store.FindAll(ctx, tags, limit)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tfor i := range users {\n\t\tusers[i].Secret = \"\"\n\t}\n\n\treturn users, nil\n}\n\n// Get finds a user by name.\nfunc (ret *Retriever) Get(ctx context.Context, name string) (*domain.User, error) {\n\treturn ret.findUser(ctx, name, true)\n}\n\n// VerifySecret finds the user by name and verifies the secret against the has found\n// in the store.\nfunc (ret *Retriever) VerifySecret(ctx context.Context, name, secret string) bool {\n\tuser, err := ret.findUser(ctx, name, false)\n\tif err != nil {\n\t\treturn false\n\t}\n\n\treturn user.CheckSecret(secret)\n}\n\nfunc (ret *Retriever) findUser(ctx context.Context, name string, stripSecret bool) (*domain.User, error) {\n\tuser, err := ret.store.FindByName(ctx, name)\n\tif err != nil {\n\t\tret.Debugf(\"failed to find user with name '%s': %v\", name, err)\n\t\treturn nil, err\n\t}\n\n\tif stripSecret {\n\t\tuser.Secret = \"\"\n\t}\n\treturn user, nil\n}\n"
  },
  {
    "path": "usecases/users/store.go",
    "content": "package users\n\nimport (\n\t\"context\"\n\n\t\"github.com/spy16/droplets/domain\"\n)\n\n// Store implementation is responsible for managing persistence of\n// users.\ntype Store interface {\n\tExists(ctx context.Context, name string) bool\n\tSave(ctx context.Context, user domain.User) (*domain.User, error)\n\tFindByName(ctx context.Context, name string) (*domain.User, error)\n\tFindAll(ctx context.Context, tags []string, limit int) ([]domain.User, error)\n}\n"
  },
  {
    "path": "web/static/main.css",
    "content": "html {\n}"
  },
  {
    "path": "web/templates/index.tpl",
    "content": "<!doctype html>\n<html lang=\"en\">\n\n<head>\n  <!-- Required meta tags -->\n  <meta charset=\"utf-8\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, shrink-to-fit=no\">\n\n  <!-- Bootstrap CSS -->\n  <link rel=\"stylesheet\" href=\"https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css\" integrity=\"sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO\"\n    crossorigin=\"anonymous\">\n\n  <title>Droplets</title>\n</head>\n\n<body>\n  <div class=\"container\">\n    <nav class=\"navbar navbar-light bg-light\">\n       <a class=\"navbar-brand\">\n          <img src=\"static/favicon.ico\" width=\"30\" height=\"30\" class=\"d-inline-block align-top\" alt=\"\">\n          Droplets\n       </a>\n       <form class=\"form-inline\" action=\"search\" method=\"GET\">\n         <input class=\"form-control mr-sm-2\" type=\"search\" name=\"query\" placeholder=\"Search\" aria-label=\"Search\">\n         <button class=\"btn btn-outline-success my-2 my-sm-0\" type=\"submit\">Search</button>\n       </form>\n    </nav>\n\n    <div class=\"container\">\n      <h1>Welcome </h1>\n    </div>\n  </div>\n  <!-- Optional JavaScript -->\n  <!-- jQuery first, then Popper.js, then Bootstrap JS -->\n  <script src=\"https://code.jquery.com/jquery-3.3.1.slim.min.js\" integrity=\"sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo\"\n    crossorigin=\"anonymous\"></script>\n  <script src=\"https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js\" integrity=\"sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49\"\n    crossorigin=\"anonymous\"></script>\n  <script src=\"https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js\" integrity=\"sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy\"\n    crossorigin=\"anonymous\"></script>\n</body>\n\n</html>"
  }
]