Full Code of spy16/droplets for AI

master b94dee34e852 cached
61 files
70.1 KB
21.6k tokens
151 symbols
1 requests
Download .txt
Repository: spy16/droplets
Branch: master
Commit: b94dee34e852
Files: 61
Total size: 70.1 KB

Directory structure:
gitextract_s7jwt71_/

├── .gitignore
├── CONTRIBUTING.md
├── Dockerfile
├── LICENSE
├── Makefile
├── README.md
├── docker-compose.yml
├── docs/
│   ├── interfaces.md
│   └── organization.md
├── domain/
│   ├── doc.go
│   ├── meta.go
│   ├── meta_test.go
│   ├── post.go
│   ├── post_test.go
│   ├── user.go
│   └── user_test.go
├── go.mod
├── go.sum
├── interfaces/
│   ├── mongo/
│   │   ├── doc.go
│   │   ├── mongo.go
│   │   ├── posts.go
│   │   └── users.go
│   ├── rest/
│   │   ├── doc.go
│   │   ├── posts.go
│   │   ├── rest.go
│   │   ├── users.go
│   │   └── utils.go
│   └── web/
│       ├── app.go
│       ├── doc.go
│       ├── fs.go
│       └── web.go
├── main.go
├── pkg/
│   ├── doc.go
│   ├── errors/
│   │   ├── authorization.go
│   │   ├── doc.go
│   │   ├── error.go
│   │   ├── errors.go
│   │   ├── resource.go
│   │   ├── stack.go
│   │   └── validation.go
│   ├── graceful/
│   │   ├── doc.go
│   │   └── graceful.go
│   ├── logger/
│   │   ├── doc.go
│   │   └── logrus.go
│   ├── middlewares/
│   │   ├── authn.go
│   │   ├── doc.go
│   │   ├── logging.go
│   │   ├── recovery.go
│   │   └── utils.go
│   └── render/
│       ├── doc.go
│       └── render.go
├── usecases/
│   ├── posts/
│   │   ├── doc.go
│   │   ├── publish.go
│   │   ├── retrieval.go
│   │   └── store.go
│   └── users/
│       ├── doc.go
│       ├── registration.go
│       ├── retrieval.go
│       └── store.go
└── web/
    ├── static/
    │   └── main.css
    └── templates/
        └── index.tpl

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitignore
================================================
bin/
vendor/

# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib

# Test binary, build with `go test -c`
*.test

# Output of the go coverage tool, specifically when used with LiteIDE
*.out


.DS_Store

# temporarily do not track this
PRACTICES.md

# custom ignores
expt/
test.db
.vscode/
.idea/


================================================
FILE: CONTRIBUTING.md
================================================
# Contributing to Droplets

Thanks for taking the time to contribute. You are Awesome! :heart:

Droplets is built to showcase ideas, patterns and best practices which can be
applied while building cool stuff with Go.

## What can I contribute and How ?

A project with realistic development cycle is required to be able to demonstrate
different ideas as applicable in real-world scenarios. This means, any type of
contribution that a typical open-source project would go through are welcome here.

Some possible contributions:

- Suggest features to add
- Suggest improvements to code
- Suggest improvements to documentation
- Open an issue and discuss a practice used
- Try the project and report bugs
- Designs for the web app

Or any other contribution you can think of (And be sure to send a PR to add it to
this document, which itself is another contribution!)

You can simply follow the guidelines from [opensource.guide](https://opensource.guide/how-to-contribute/).

## Responsibilities

* No platform specific code
* 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)
* Keep PRs as small as possible to make it easy to review and discuss.
* Be welcoming to newcomers and encourage diverse new contributors from all backgrounds.
* Code change PRs must have unit tests



================================================
FILE: Dockerfile
================================================
FROM golang:1.11 as builder
RUN mkdir /droplets-src
WORKDIR /droplets-src
COPY ./ .
RUN CGO_ENABLED=0 make setup all

FROM alpine:latest
RUN mkdir /app
WORKDIR /app
COPY --from=builder /droplets-src/bin/droplets ./
COPY --from=builder /droplets-src/web ./web
EXPOSE 8080
CMD ["./droplets"]


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2018 Shivaprasad Bhat

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: Makefile
================================================
build:
	@echo "Building droplets at './bin/droplets' ..."
	@go build -o bin/droplets

clean:
	rm -rf ./bin

all: lint	vet	cyclo	test	build

test:
	@echo "Running unit tests..."
	@go test -cover ./...

cyclo:
	@echo "Checking cyclomatic complexity..."
	@gocyclo -over 7 ./

vet:
	@echo "Running vet..."
	@go vet ./...

lint:
	@echo "Running golint..."
	@golint ./...

setup:
	@go get -u golang.org/x/lint/golint
	@go get -u github.com/fzipp/gocyclo

================================================
FILE: README.md
================================================
> WIP

# droplets

[![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)
[![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)

A platform for Gophers similar to the awesome [Golang News](http://golangnews.com).

## Why?

Droplets is built to showcase:

1. Application of [CodeReviewComments](https://github.com/golang/go/wiki/CodeReviewComments) and [EffectiveGo](https://golang.org/doc/effective_go.html)
2. Usage of [Clean Architecture](http://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html)
3. Testing practices such as [Table-driven tests](https://github.com/golang/go/wiki/TableDrivenTests)

Follow the links to understand best practices and conventions used:
1. [Directory Structure](./docs/organization.md)
2. [Interfaces](./docs/interfaces.md)

## Building

Droplets uses `go mod` (available from `go 1.11`) for dependency management.

To test and build, run `make all`.

## License
[![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)


================================================
FILE: docker-compose.yml
================================================
version: '3.2'

services:
  droplets:
    build: ./
    environment:
      - MONGO_URI=mongodb://mongo
      - LOG_LEVEL=info
    ports:
      - "8080:8080"
    links:
      - mongo
    networks:
      - droplets_net
  mongo:
    image: mongo:3-stretch
    ports:
      - "27017:27017"
    networks:
      - droplets_net

networks:
  droplets_net:


================================================
FILE: docs/interfaces.md
================================================

# Interfaces

Following are some best practices for using interfaces:

1. Define small interfaces with well defined scope
   - Single-method interfaces are ideal (e.g. `io.Reader`, `io.Writer` etc.)
   - [Bigger the interface, weaker the abstraction - Go Proverbs by Rob Pike](https://www.youtube.com/watch?v=PAAkCSZUG1c&t=5m17s)
2. Accept interfaces, return structs
   - Interfaces should be defined where they are used [Read More](#where-should-i-define-the-interface-)

## Where should I define the interface ?

From [CodeReviewComments](https://github.com/golang/go/wiki/CodeReviewComments#interfaces):

> Go interfaces generally belong in the package that uses values of the interface type, not the
> package that implements those values.

Interfaces are contracts that should be used to define the minimal requirement of a client
to execute its functionality. In other words, client defines what it needs and not the
implementor. So, interfaces should generally be defined on the client side. This is also inline
with the [Interface Segregation Principle](https://en.wikipedia.org/wiki/Interface_segregation_principle)
from [SOLID](https://en.wikipedia.org/wiki/SOLID) principles.

A **bad** pattern that shows up quite a lot:

```go
package producer

func NewThinger() Thinger {
    return defaultThinger{ … }
}

type Thinger interface {
    Thing() bool
}


type defaultThinger struct{ … }
func (t defaultThinger) Thing() bool { … }
```

### Why is this bad?

Go uses [Structural Type System](https://en.wikipedia.org/wiki/Structural_type_system) as opposed to
[Nominal Type System](https://en.wikipedia.org/wiki/Nominal_type_system) used in other static languages
like `Java`, `C#` etc. This simply means that a type `MyType` does not need to add `implements Doer` clause
to be compatible with an interface `Doer`. `MyType` is compatible with `Doer` interface if it has all the
methods defined in `Doer`.

Read following articles for more information:

1. https://medium.com/@cep21/preemptive-interface-anti-pattern-in-go-54c18ac0668a
2. https://medium.com/@cep21/what-accept-interfaces-return-structs-means-in-go-2fe879e25ee8

This also provides an interesting power to Go interfaces. Clients are truly free to define interfaces when they
need to. For example consider the following function:

```go
func writeData(f *os.File, data string) {
    f.Write([]byte(data))
}
```

Let's assume after sometime a new feature requirement which requires us to write to a tcp connection. One
thing we could do is define a new function:

```go
func writeDataToTCPCon(con *net.TCPConn, data string) {
    con.Write([]byte(data))
}
```

But this approach is tedious and will grow out of control quickly as new requirements are added. Also, different
writers cannot be injected into other entities easily. But instead, you can simply refactor the `writeData` function
as below:

```go
type writer interface {
    Write([]byte) (int, error)
}

func writeData(wr writer, data string) {
    wr.Write([]byte(data))
}
```

Refactored `writeData` will continue to work with our existing code that is passing `*os.File` since it
implements `writer`. In addition, `writeData` function can now accept anything that implements `writer`
which includes `os.File`, `net.TCPConn`, `http.ResponseWriter` etc. (And every single Go entity in the
**entire world** that has a method `Write([]byte) (int, error)`)

Note that, this pattern is *not possible in other languages*. Because, after refactoring `writeData` to
accept a new interface `writer`, you need to refactor all the classes you want to use with `writeData` to
have `implements writer` in their declarations.

Another advantage is that client is free to define the subset of features it requires instead of accepting
more than it needs.




================================================
FILE: docs/organization.md
================================================

# Directory Structure

Directory structure is based on [Clean Architecture](http://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html).

![Clean Architecture](http://blog.cleancoder.com/uncle-bob/images/2012-08-13-the-clean-architecture/CleanArchitecture.jpg)

Most important part of Clean Architecture is the **Dependency Rule**:

> **source code dependencies can only point inwards**

### 1. `domain/`

Package `domain` represents the `entities` layer from the Clean Architecture. Entities in this layer are
*least likely to change when something external changes*. For example, the rules inside `Validate` methods
are designed in such a way that the these are *absolute requirement* for the entity to belong to the domain.
For example, a `User` object without `Name` does not belong to `domain` of Droplets.

In other words, any feature or business requirement that comes in later, these definitions would still not
change unless the requirement is leading to a domain change.

This package **strictly cannot** have direct dependency on external packages. It can use built-in types,
functions and standard library types/functions.

> `domain` package makes one exception and imports `github.com/spy16/droplets/pkg/errors`. This is because
> of errors are values and are a basic requirement across the application. In other words, the `errors` package
> is used in place of `errors` package from the standard library.

### 2. `usecases/`

Directory (not a package) `usecases` represents the `Use Cases` layer from the Clean Architecture. It encapsulates
and implements all of the use cases of the system by directing the `entities` layer. This layer is expected to change
when a new use case or business requirement is presented.

In Droplets, Use cases are separated as packages based on the entity they primarily operate on. (e.g. `usecases/users` etc.)

Any real use case would also need external entities such as persistence, external service integration etc.
But this layer also **strictly** cannot have direct dependency on external packages. This crossing of boundaries
is done through interfaces.

For example, `users.Registrar` provides functions for registering users which requires storage functionality. But
this cannot directly import a `mongo` or `sql` driver and implement storage functions. It can also not import an
adapter from the `interfaces` package directly both of which would violate `Dependency Rule`. So instead, an interface
`users.Store` is defined which is expected to injected when calling `NewRegistrar`.

**Why is `Store` interface defined in `users` package?**

See [Interfaces](interfaces.md) for conventions around interfaces.


### 3. `interfaces/`

> Should not be confused with Go `interface` keyword.

- Represents the `interface-adapter` layer from the Clean Architecture
- This is the layer that cares about the external world (i.e, external dependencies).
- Interfacing includes:
    - Exposing `usecases` as API (e.g., RPC, GraphQL, REST etc.)
    - Presenting `usecases` to end-user (e.g., GUI, WebApp etc.)
    - Persistence logic (e.g., cache, datastores etc.)
    - Integrating an external service required by `usecases`
- Packages inside this are organized in 2 ways:
    1. Based on the medium they use (e.g., `rest`, `web` etc.)
    2. Based on the external dependency they use (e.g., `mongo`, `redis` etc.)

### 4. `pkg/`

- Contains re-usable packages that is safe to be imported in other projects
- This package should not import anything from `domain`, `interfaces`, `usecases` or their sub-packages


### 5. `web/`

- `web/` is **NOT** a Go package
- Contains web assets such as css, images, templates etc.

================================================
FILE: domain/doc.go
================================================
// Package domain contains domain entities and core validation rules.
package domain


================================================
FILE: domain/meta.go
================================================
package domain

import (
	"strings"
	"time"

	"github.com/spy16/droplets/pkg/errors"
)

// Meta represents metadata about different entities.
type Meta struct {
	// Name represents a unique name/identifier for the object.
	Name string `json:"name" bson:"name"`

	// Tags can contain additional metadata about the object.
	Tags []string `json:"tags,omitempty" bson:"tags"`

	// CreateAt represents the time at which this object was created.
	CreatedAt time.Time `json:"created_at,omitempty" bson:"created_at"`

	// UpdatedAt represents the time at which this object was last
	// modified.
	UpdatedAt time.Time `json:"updated_at,omitempty" bson:"updated_at"`
}

// SetDefaults sets sensible defaults on meta.
func (meta *Meta) SetDefaults() {
	if meta.CreatedAt.IsZero() {
		meta.CreatedAt = time.Now()
		meta.UpdatedAt = time.Now()
	}
}

// Validate performs basic validation of the metadata.
func (meta Meta) Validate() error {
	switch {
	case empty(meta.Name):
		return errors.MissingField("Name")
	}
	return nil
}

func empty(str string) bool {
	return len(strings.TrimSpace(str)) == 0
}


================================================
FILE: domain/meta_test.go
================================================
package domain_test

import (
	"fmt"
	"testing"

	"github.com/spy16/droplets/domain"
	"github.com/spy16/droplets/pkg/errors"
)

func TestMeta_Validate(suite *testing.T) {
	suite.Parallel()

	cases := []struct {
		meta      domain.Meta
		expectErr bool
		errType   string
	}{}

	for id, cs := range cases {
		suite.Run(fmt.Sprintf("Case#%d", id), func(t *testing.T) {
			testValidation(t, cs.meta, cs.expectErr, cs.errType)
		})
	}

}

func testValidation(t *testing.T, validator validatable, expectErr bool, errType string) {
	err := validator.Validate()
	if err != nil {
		if !expectErr {
			t.Errorf("unexpected error: %s", err)
			return
		}

		if actualType := errors.Type(err); actualType != errType {
			t.Errorf("expecting error type '%s', got '%s'", errType, actualType)
		}
		return
	}

	if expectErr {
		t.Errorf("was expecting an error of type '%s', got nil", errType)
		return
	}
}

type validatable interface {
	Validate() error
}


================================================
FILE: domain/post.go
================================================
package domain

import (
	"fmt"
	"strings"

	"github.com/spy16/droplets/pkg/errors"
)

// Common content types.
const (
	ContentLibrary = "library"
	ContentLink    = "link"
	ContentVideo   = "video"
)

var validTypes = []string{ContentLibrary, ContentLink, ContentVideo}

// Post represents an article, link, video etc.
type Post struct {
	Meta `json:",inline" bson:",inline"`

	// Type should state the type of the content. (e.g., library,
	// video, link etc.)
	Type string `json:"type" bson:"type"`

	// Body should contain the actual content according to the Type
	// specified. (e.g. github.com/spy16/parens when Type=link)
	Body string `json:"body" bson:"body"`

	// Owner represents the name of the user who created the post.
	Owner string `json:"owner" bson:"owner"`
}

// Validate performs validation of the post.
func (post Post) Validate() error {
	if err := post.Meta.Validate(); err != nil {
		return err
	}

	if len(strings.TrimSpace(post.Body)) == 0 {
		return errors.MissingField("Body")
	}

	if len(strings.TrimSpace(post.Owner)) == 0 {
		return errors.MissingField("Owner")
	}

	if !contains(post.Type, validTypes) {
		return errors.InvalidValue("Type", fmt.Sprintf("type must be one of: %s", strings.Join(validTypes, ",")))
	}

	return nil
}

func contains(val string, vals []string) bool {
	for _, item := range vals {
		if val == item {
			return true
		}
	}
	return false
}


================================================
FILE: domain/post_test.go
================================================
package domain_test

import (
	"fmt"
	"testing"

	"github.com/spy16/droplets/domain"
)

func TestPost_Validate(suite *testing.T) {
	suite.Parallel()

	validMeta := domain.Meta{
		Name: "hello",
	}

	cases := []struct {
		post      domain.Post
		expectErr bool
	}{
		{
			post:      domain.Post{},
			expectErr: true,
		},
		{
			post: domain.Post{
				Meta: validMeta,
			},
			expectErr: true,
		},
		{
			post: domain.Post{
				Meta: validMeta,
				Body: "hello world post!",
			},
			expectErr: true,
		},
		{
			post: domain.Post{
				Meta:  validMeta,
				Type:  "blah",
				Owner: "spy16",
				Body:  "hello world post!",
			},
			expectErr: true,
		},
		{
			post: domain.Post{
				Meta: validMeta,
				Type: domain.ContentLibrary,
				Body: "hello world post!",
			},
			expectErr: true,
		},
		{
			post: domain.Post{
				Meta:  validMeta,
				Type:  domain.ContentLibrary,
				Body:  "hello world post!",
				Owner: "spy16",
			},
			expectErr: false,
		},
	}

	for id, cs := range cases {
		suite.Run(fmt.Sprintf("#%d", id), func(t *testing.T) {
			err := cs.post.Validate()
			if err != nil {
				if !cs.expectErr {
					t.Errorf("was not expecting error, got '%s'", err)
				}
				return
			}

			if cs.expectErr {
				t.Errorf("was expecting error, got nil")
			}
		})
	}
}


================================================
FILE: domain/user.go
================================================
package domain

import (
	"net/mail"

	"github.com/spy16/droplets/pkg/errors"
	"golang.org/x/crypto/bcrypt"
)

// User represents information about registered users.
type User struct {
	Meta `json:",inline,omitempty" bson:",inline"`

	// Email should contain a valid email of the user.
	Email string `json:"email,omitempty" bson:"email"`

	// Secret represents the user secret.
	Secret string `json:"secret,omitempty" bson:"secret"`
}

// Validate performs basic validation of user information.
func (user User) Validate() error {
	if err := user.Meta.Validate(); err != nil {
		return err
	}

	_, err := mail.ParseAddress(user.Email)
	if err != nil {
		return errors.InvalidValue("Email", err.Error())
	}

	return nil
}

// HashSecret creates bcrypt hash of the password.
func (user *User) HashSecret() error {
	bytes, err := bcrypt.GenerateFromPassword([]byte(user.Secret), 4)
	if err != nil {
		return err
	}
	user.Secret = string(bytes)
	return nil
}

// CheckSecret compares the cleartext password with the hash.
func (user User) CheckSecret(password string) bool {
	err := bcrypt.CompareHashAndPassword([]byte(user.Secret), []byte(password))
	return err == nil
}


================================================
FILE: domain/user_test.go
================================================
package domain_test

import (
	"fmt"
	"testing"

	"github.com/spy16/droplets/domain"
	"github.com/spy16/droplets/pkg/errors"
)

func TestUser_CheckSecret(t *testing.T) {
	password := "hello@world!"

	user := domain.User{}
	user.Secret = password
	err := user.HashSecret()
	if err != nil {
		t.Errorf("was not expecting error, got '%s'", err)
	}

	if !user.CheckSecret(password) {
		t.Errorf("CheckSecret expected to return true, but got false")
	}
}

func TestUser_Validate(suite *testing.T) {
	suite.Parallel()

	cases := []struct {
		user      domain.User
		expectErr bool
		errType   string
	}{
		{
			user:      domain.User{},
			expectErr: true,
			errType:   errors.TypeMissingField,
		},
		{
			user: domain.User{
				Meta: domain.Meta{
					Name: "spy16",
				},
				Email: "blah.com",
			},
			expectErr: true,
			errType:   errors.TypeInvalidValue,
		},
		{
			user: domain.User{
				Meta: domain.Meta{
					Name: "spy16",
				},
				Email: "spy16 <no-mail@nomail.com>",
			},
			expectErr: false,
		},
	}

	for id, cs := range cases {
		suite.Run(fmt.Sprintf("Case#%d", id), func(t *testing.T) {
			testValidation(t, cs.user, cs.expectErr, cs.errType)
		})
	}
}


================================================
FILE: go.mod
================================================
module github.com/spy16/droplets

go 1.15

require (
	github.com/BurntSushi/toml v0.3.1 // indirect
	github.com/gorilla/context v1.1.1 // indirect
	github.com/gorilla/mux v1.6.2
	github.com/kr/pretty v0.1.0 // indirect
	github.com/mitchellh/mapstructure v1.1.2 // indirect
	github.com/pelletier/go-toml v1.2.0
	github.com/sirupsen/logrus v1.2.0
	github.com/spf13/cast v1.3.0 // indirect
	github.com/spf13/pflag v1.0.3 // indirect
	github.com/spf13/viper v1.2.1
	github.com/unrolled/render v0.0.0-20180914162206-b9786414de4d
	golang.org/x/crypto v0.0.0-20181106171534-e4dc69e5b2fd
	golang.org/x/sys v0.0.0-20181116161606-93218def8b18 // indirect
	gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
	gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce
)


================================================
FILE: go.sum
================================================
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/mux v1.6.2 h1:Pgr17XVTNXAk3q/r4CpKzC5xBM/qW1uVLV+IhRZpIIk=
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mitchellh/mapstructure v1.0.0/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sirupsen/logrus v1.2.0 h1:juTguoYk5qI21pwyTXY3B3Y5cOTH3ZUyZCg1v/mihuo=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/cast v1.2.0/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg=
github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/pflag v1.0.2/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/viper v1.2.1 h1:bIcUwXqLseLF3BDAZduuNfekWG87ibtFxi59Bq+oI9M=
github.com/spf13/viper v1.2.1/go.mod h1:P4AexN0a+C9tGAnUFNwDMYYZv3pjFuvmeiMyKRaNVlI=
github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/unrolled/render v0.0.0-20180914162206-b9786414de4d h1:ggUgChAeyge4NZ4QUw6lhHsVymzwSDJOZcE0s2X8S20=
github.com/unrolled/render v0.0.0-20180914162206-b9786414de4d/go.mod h1:tu82oB5W2ykJRVioYsB+IQKcft7ryBr7w12qMBUPyXg=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793 h1:u+LnwYTOOW7Ukr/fppxEb1Nwz0AtPflrblfvUudpo+I=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181106171534-e4dc69e5b2fd h1:VtIkGDhk0ph3t+THbvXHfMZ8QHgsBO39Nh52+74pq7w=
golang.org/x/crypto v0.0.0-20181106171534-e4dc69e5b2fd/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33 h1:I6FyU15t786LL7oL/hn43zqTuEGr4PN7F4XJ1p4E3Y8=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180906133057-8cf3aee42992 h1:BH3eQWeGbwRU2+wxxuuPOdFBmaiBH81O8BugSjHeTFg=
golang.org/x/sys v0.0.0-20180906133057-8cf3aee42992/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116161606-93218def8b18 h1:Wh+XCfg3kNpjhdq2LXrsiOProjtQZKme5XUx7VcxwAw=
golang.org/x/sys v0.0.0-20181116161606-93218def8b18/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce h1:xcEWjVhvbDy+nHP67nPDDpbYrY+ILlfndk4bRioVHaU=
gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA=
gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=


================================================
FILE: interfaces/mongo/doc.go
================================================
// Package mongo contains any component in the entire project which interfaces
// with MongoDB (e.g. different store implementations).
package mongo


================================================
FILE: interfaces/mongo/mongo.go
================================================
package mongo

import (
	"gopkg.in/mgo.v2"
)

// Connect to a MongoDB instance located by mongo-uri using the `mgo`
// driver.
func Connect(uri string, failFast bool) (*mgo.Database, func(), error) {
	di, err := mgo.ParseURL(uri)
	if err != nil {
		return nil, doNothing, err
	}

	di.FailFast = failFast
	session, err := mgo.DialWithInfo(di)
	if err != nil {
		return nil, doNothing, err
	}

	return session.DB(di.Database), session.Close, nil
}

func doNothing() {}


================================================
FILE: interfaces/mongo/posts.go
================================================
package mongo

import (
	"context"
	"time"

	"github.com/spy16/droplets/domain"
	"github.com/spy16/droplets/pkg/errors"
	"gopkg.in/mgo.v2"
	"gopkg.in/mgo.v2/bson"
)

const colPosts = "posts"

// NewPostStore initializes the Posts store with given mongo db handle.
func NewPostStore(db *mgo.Database) *PostStore {
	return &PostStore{
		db: db,
	}
}

// PostStore manages persistence and retrieval of posts.
type PostStore struct {
	db *mgo.Database
}

// Exists checks if a post exists by name.
func (posts *PostStore) Exists(ctx context.Context, name string) bool {
	col := posts.db.C(colPosts)

	count, err := col.Find(bson.M{"name": name}).Count()
	if err != nil {
		return false
	}
	return count > 0
}

// Get finds a post by name.
func (posts *PostStore) Get(ctx context.Context, name string) (*domain.Post, error) {
	col := posts.db.C(colPosts)

	post := domain.Post{}
	if err := col.Find(bson.M{"name": name}).One(&post); err != nil {
		if err == mgo.ErrNotFound {
			return nil, errors.ResourceNotFound("Post", name)
		}
		return nil, errors.Wrapf(err, "failed to fetch post")
	}

	post.SetDefaults()
	return &post, nil
}

// Save validates and persists the post.
func (posts *PostStore) Save(ctx context.Context, post domain.Post) (*domain.Post, error) {
	post.SetDefaults()
	if err := post.Validate(); err != nil {
		return nil, err
	}
	post.CreatedAt = time.Now()
	post.UpdatedAt = time.Now()

	col := posts.db.C(colPosts)
	if err := col.Insert(post); err != nil {
		return nil, err
	}
	return &post, nil
}

// Delete removes one post identified by the name.
func (posts *PostStore) Delete(ctx context.Context, name string) (*domain.Post, error) {
	col := posts.db.C(colPosts)

	ch := mgo.Change{
		Remove:    true,
		ReturnNew: true,
		Upsert:    false,
	}
	post := domain.Post{}
	_, err := col.Find(bson.M{"name": name}).Apply(ch, &post)
	if err != nil {
		if err == mgo.ErrNotFound {
			return nil, errors.ResourceNotFound("Post", name)
		}
		return nil, err
	}

	return &post, nil
}


================================================
FILE: interfaces/mongo/users.go
================================================
package mongo

import (
	"context"
	"time"

	"github.com/spy16/droplets/domain"
	"github.com/spy16/droplets/pkg/errors"
	"gopkg.in/mgo.v2"
	"gopkg.in/mgo.v2/bson"
)

const colUsers = "users"

// NewUserStore initializes a users store with the given db handle.
func NewUserStore(db *mgo.Database) *UserStore {
	return &UserStore{
		db: db,
	}
}

// UserStore provides functions for persisting User entities in MongoDB.
type UserStore struct {
	db *mgo.Database
}

// Exists checks if the user identified by the given username already
// exists. Will return false in case of any error.
func (users *UserStore) Exists(ctx context.Context, name string) bool {
	col := users.db.C(colUsers)

	count, err := col.Find(bson.M{"name": name}).Count()
	if err != nil {
		return false
	}
	return count > 0
}

// Save validates and persists the user.
func (users *UserStore) Save(ctx context.Context, user domain.User) (*domain.User, error) {
	user.SetDefaults()
	if err := user.Validate(); err != nil {
		return nil, err
	}
	user.CreatedAt = time.Now()
	user.UpdatedAt = time.Now()

	col := users.db.C(colUsers)
	if err := col.Insert(user); err != nil {
		return nil, err
	}
	return &user, nil
}

// FindByName finds a user by name. If not found, returns ResourceNotFound error.
func (users *UserStore) FindByName(ctx context.Context, name string) (*domain.User, error) {
	col := users.db.C(colUsers)

	user := domain.User{}
	if err := col.Find(bson.M{"name": name}).One(&user); err != nil {
		if err == mgo.ErrNotFound {
			return nil, errors.ResourceNotFound("User", name)
		}
		return nil, errors.Wrapf(err, "failed to fetch user")
	}

	user.SetDefaults()
	return &user, nil
}

// FindAll finds all users matching the tags.
func (users *UserStore) FindAll(ctx context.Context, tags []string, limit int) ([]domain.User, error) {
	col := users.db.C(colUsers)

	filter := bson.M{}
	if len(tags) > 0 {
		filter["tags"] = bson.M{
			"$in": tags,
		}
	}

	matches := []domain.User{}
	if err := col.Find(filter).Limit(limit).All(&matches); err != nil {
		return nil, errors.Wrapf(err, "failed to query for users")
	}
	return matches, nil
}

// Delete removes one user identified by the name.
func (users *UserStore) Delete(ctx context.Context, name string) (*domain.User, error) {
	col := users.db.C(colUsers)

	ch := mgo.Change{
		Remove:    true,
		ReturnNew: true,
		Upsert:    false,
	}
	user := domain.User{}
	_, err := col.Find(bson.M{"name": name}).Apply(ch, &user)
	if err != nil {
		if err == mgo.ErrNotFound {
			return nil, errors.ResourceNotFound("User", name)
		}
		return nil, err
	}

	return &user, nil
}


================================================
FILE: interfaces/rest/doc.go
================================================
// Package rest exposes the features of droplets as REST API. This
// package uses gorilla/mux for routing.
package rest


================================================
FILE: interfaces/rest/posts.go
================================================
package rest

import (
	"context"
	"net/http"

	"github.com/gorilla/mux"
	"github.com/spy16/droplets/domain"
	"github.com/spy16/droplets/pkg/logger"
	"github.com/spy16/droplets/pkg/middlewares"
	"github.com/spy16/droplets/usecases/posts"
)

func addPostsAPI(router *mux.Router, pub postPublication, ret postRetriever, lg logger.Logger) {
	pc := &postController{}
	pc.ret = ret
	pc.pub = pub
	pc.Logger = lg

	router.HandleFunc("/v1/posts", pc.search).Methods(http.MethodGet)
	router.HandleFunc("/v1/posts/{name}", pc.get).Methods(http.MethodGet)
	router.HandleFunc("/v1/posts/{name}", pc.delete).Methods(http.MethodDelete)
	router.HandleFunc("/v1/posts", pc.post).Methods(http.MethodPost)
}

type postController struct {
	logger.Logger

	pub postPublication
	ret postRetriever
}

func (pc *postController) search(wr http.ResponseWriter, req *http.Request) {
	posts, err := pc.ret.Search(req.Context(), posts.Query{})
	if err != nil {
		respondErr(wr, err)
		return
	}

	respond(wr, http.StatusOK, posts)
}

func (pc *postController) get(wr http.ResponseWriter, req *http.Request) {
	name := mux.Vars(req)["name"]
	post, err := pc.ret.Get(req.Context(), name)
	if err != nil {
		respondErr(wr, err)
		return
	}

	respond(wr, http.StatusOK, post)
}

func (pc *postController) post(wr http.ResponseWriter, req *http.Request) {
	post := domain.Post{}
	if err := readRequest(req, &post); err != nil {
		pc.Warnf("failed to read user request: %s", err)
		respond(wr, http.StatusBadRequest, err)
		return
	}
	user, _ := middlewares.User(req)
	post.Owner = user

	published, err := pc.pub.Publish(req.Context(), post)
	if err != nil {
		respondErr(wr, err)
		return
	}

	respond(wr, http.StatusCreated, published)
}

func (pc *postController) delete(wr http.ResponseWriter, req *http.Request) {
	name := mux.Vars(req)["name"]
	post, err := pc.pub.Delete(req.Context(), name)
	if err != nil {
		respondErr(wr, err)
		return
	}

	respond(wr, http.StatusOK, post)
}

type postRetriever interface {
	Get(ctx context.Context, name string) (*domain.Post, error)
	Search(ctx context.Context, query posts.Query) ([]domain.Post, error)
}

type postPublication interface {
	Publish(ctx context.Context, post domain.Post) (*domain.Post, error)
	Delete(ctx context.Context, name string) (*domain.Post, error)
}


================================================
FILE: interfaces/rest/rest.go
================================================
package rest

import (
	"net/http"

	"github.com/spy16/droplets/pkg/render"

	"github.com/gorilla/mux"
	"github.com/spy16/droplets/pkg/errors"
	"github.com/spy16/droplets/pkg/logger"
)

// New initializes the server with routes exposing the given usecases.
func New(logger logger.Logger, reg registration, ret retriever, postsRet postRetriever, postPub postPublication) http.Handler {
	// setup router with default handlers
	router := mux.NewRouter()
	router.NotFoundHandler = http.HandlerFunc(notFoundHandler)
	router.MethodNotAllowedHandler = http.HandlerFunc(methodNotAllowedHandler)

	// setup api endpoints
	addUsersAPI(router, reg, ret, logger)
	addPostsAPI(router, postPub, postsRet, logger)

	return router
}

func notFoundHandler(wr http.ResponseWriter, req *http.Request) {
	render.JSON(wr, http.StatusNotFound, errors.ResourceNotFound("path", req.URL.Path))
}

func methodNotAllowedHandler(wr http.ResponseWriter, req *http.Request) {
	render.JSON(wr, http.StatusMethodNotAllowed, errors.ResourceNotFound("path", req.URL.Path))
}


================================================
FILE: interfaces/rest/users.go
================================================
package rest

import (
	"context"
	"net/http"

	"github.com/gorilla/mux"
	"github.com/spy16/droplets/domain"
	"github.com/spy16/droplets/pkg/logger"
)

func addUsersAPI(router *mux.Router, reg registration, ret retriever, logger logger.Logger) {
	uc := &userController{
		Logger: logger,
		reg:    reg,
		ret:    ret,
	}

	router.HandleFunc("/v1/users/{name}", uc.get).Methods(http.MethodGet)
	router.HandleFunc("/v1/users/", uc.search).Methods(http.MethodGet)
	router.HandleFunc("/v1/users/", uc.post).Methods(http.MethodPost)
}

type userController struct {
	logger.Logger
	reg registration
	ret retriever
}

func (uc *userController) get(wr http.ResponseWriter, req *http.Request) {
	vars := mux.Vars(req)
	user, err := uc.ret.Get(req.Context(), vars["name"])
	if err != nil {
		respondErr(wr, err)
		return
	}

	respond(wr, http.StatusOK, user)
}

func (uc *userController) search(wr http.ResponseWriter, req *http.Request) {
	vals := req.URL.Query()["t"]
	users, err := uc.ret.Search(req.Context(), vals, 10)
	if err != nil {
		respondErr(wr, err)
		return
	}

	respond(wr, http.StatusOK, users)
}

func (uc *userController) post(wr http.ResponseWriter, req *http.Request) {
	user := domain.User{}
	if err := readRequest(req, &user); err != nil {
		uc.Warnf("failed to read user request: %s", err)
		respond(wr, http.StatusBadRequest, err)
		return
	}

	registered, err := uc.reg.Register(req.Context(), user)
	if err != nil {
		uc.Warnf("failed to register user: %s", err)
		respondErr(wr, err)
		return
	}

	uc.Infof("new user registered with id '%s'", registered.Name)
	respond(wr, http.StatusCreated, registered)
}

type registration interface {
	Register(ctx context.Context, user domain.User) (*domain.User, error)
}

type retriever interface {
	Get(ctx context.Context, name string) (*domain.User, error)
	Search(ctx context.Context, tags []string, limit int) ([]domain.User, error)
	VerifySecret(ctx context.Context, name, secret string) bool
}


================================================
FILE: interfaces/rest/utils.go
================================================
package rest

import (
	"encoding/json"
	"net/http"

	"github.com/spy16/droplets/pkg/errors"
	"github.com/spy16/droplets/pkg/render"
)

func respond(wr http.ResponseWriter, status int, v interface{}) {
	if err := render.JSON(wr, status, v); err != nil {
		if loggable, ok := wr.(errorLogger); ok {
			loggable.Errorf("failed to write data to http ResponseWriter: %s", err)
		}
	}
}

func respondErr(wr http.ResponseWriter, err error) {
	if e, ok := err.(*errors.Error); ok {
		respond(wr, e.Code, e)
		return
	}
	respond(wr, http.StatusInternalServerError, err)
}

func readRequest(req *http.Request, v interface{}) error {
	if err := json.NewDecoder(req.Body).Decode(v); err != nil {
		return errors.Validation("Failed to read request body")
	}

	return nil
}

type errorLogger interface {
	Errorf(msg string, args ...interface{})
}


================================================
FILE: interfaces/web/app.go
================================================
package web

import (
	"html/template"
	"net/http"
)

type app struct {
	render func(wr http.ResponseWriter, tpl string, data interface{})
	tpl    template.Template
}

func (app app) indexHandler(wr http.ResponseWriter, req *http.Request) {
	app.render(wr, "index.tpl", nil)
}


================================================
FILE: interfaces/web/doc.go
================================================
// Package web contains MVC style web app.
package web


================================================
FILE: interfaces/web/fs.go
================================================
package web

import (
	"net/http"
	"os"

	"github.com/spy16/droplets/pkg/logger"
)

func newSafeFileSystemServer(lg logger.Logger, root string) http.Handler {
	sfs := &safeFileSystem{
		fs:     http.Dir(root),
		Logger: lg,
	}
	return http.FileServer(sfs)
}

// safeFileSystem implements http.FileSystem. It is used to prevent directory
// listing of static assets.
type safeFileSystem struct {
	logger.Logger

	fs http.FileSystem
}

func (sfs safeFileSystem) Open(path string) (http.File, error) {
	f, err := sfs.fs.Open(path)
	if err != nil {
		sfs.Warnf("failed to open file '%s': %v", path, err)
		return nil, err
	}

	stat, err := f.Stat()
	if err != nil {
		return nil, err
	}
	if stat.IsDir() {
		sfs.Warnf("path '%s' is a directory, rejecting static path request", path)
		return nil, os.ErrNotExist
	}

	return f, nil
}


================================================
FILE: interfaces/web/web.go
================================================
package web

import (
	"html/template"
	"io/ioutil"
	"net/http"
	"path/filepath"

	"github.com/gorilla/mux"
	"github.com/spy16/droplets/pkg/logger"
)

// New initializes a new webapp server.
func New(lg logger.Logger, cfg Config) (http.Handler, error) {
	tpl, err := initTemplate(lg, "", cfg.TemplateDir)
	if err != nil {
		return nil, err
	}

	app := &app{
		render: func(wr http.ResponseWriter, tplName string, data interface{}) {
			if err := tpl.ExecuteTemplate(wr, tplName, data); err != nil {
				lg.Errorf("failed to render template '%s': %+v", tplName, err)
			}
		},
	}

	fsServer := newSafeFileSystemServer(lg, cfg.StaticDir)

	router := mux.NewRouter()
	router.PathPrefix("/static").Handler(http.StripPrefix("/static", fsServer))
	router.Handle("/favicon.ico", fsServer)

	// web app routes
	router.HandleFunc("/", app.indexHandler)

	return router, nil
}

// Config represents server configuration.
type Config struct {
	TemplateDir string
	StaticDir   string
}

func initTemplate(lg logger.Logger, name, path string) (*template.Template, error) {
	apath, err := filepath.Abs(path)
	if err != nil {
		return nil, err
	}

	files, err := ioutil.ReadDir(apath)
	if err != nil {
		return nil, err
	}

	lg.Infof("loading templates from '%s'...", path)
	tpl := template.New(name)
	for _, f := range files {
		if f.IsDir() {
			continue
		}
		fp := filepath.Join(apath, f.Name())
		lg.Debugf("parsing template file '%s'", f.Name())
		tpl.New(f.Name()).ParseFiles(fp)
	}

	return tpl, nil
}


================================================
FILE: main.go
================================================
package main

import (
	"context"
	"net/http"
	"os"
	"time"

	"github.com/gorilla/mux"
	"github.com/spf13/viper"
	"github.com/spy16/droplets/interfaces/mongo"
	"github.com/spy16/droplets/interfaces/rest"
	"github.com/spy16/droplets/interfaces/web"
	"github.com/spy16/droplets/pkg/graceful"
	"github.com/spy16/droplets/pkg/logger"
	"github.com/spy16/droplets/pkg/middlewares"
	"github.com/spy16/droplets/usecases/posts"
	"github.com/spy16/droplets/usecases/users"
)

func main() {
	cfg := loadConfig()
	lg := logger.New(os.Stderr, cfg.LogLevel, cfg.LogFormat)

	db, closeSession, err := mongo.Connect(cfg.MongoURI, true)
	if err != nil {
		lg.Fatalf("failed to connect to mongodb: %v", err)
	}
	defer closeSession()

	lg.Debugf("setting up rest api service")
	userStore := mongo.NewUserStore(db)
	postStore := mongo.NewPostStore(db)

	userRegistration := users.NewRegistrar(lg, userStore)
	userRetriever := users.NewRetriever(lg, userStore)

	postPub := posts.NewPublication(lg, postStore, userStore)
	postRet := posts.NewRetriever(lg, postStore)

	restHandler := rest.New(lg, userRegistration, userRetriever, postRet, postPub)
	webHandler, err := web.New(lg, web.Config{
		TemplateDir: cfg.TemplateDir,
		StaticDir:   cfg.StaticDir,
	})
	if err != nil {
		lg.Fatalf("failed to setup web handler: %v", err)
	}

	srv := setupServer(cfg, lg, webHandler, restHandler)
	lg.Infof("listening for requests on :8080...")
	if err := srv.ListenAndServe(); err != nil {
		lg.Fatalf("http server exited: %s", err)
	}
}

func setupServer(cfg config, lg logger.Logger, web http.Handler, rest http.Handler) *graceful.Server {
	rest = middlewares.WithBasicAuth(lg, rest,
		middlewares.UserVerifierFunc(func(ctx context.Context, name, secret string) bool {
			return secret == "secret@123"
		}),
	)

	router := mux.NewRouter()
	router.PathPrefix("/api").Handler(http.StripPrefix("/api", rest))
	router.PathPrefix("/").Handler(web)

	handler := middlewares.WithRequestLogging(lg, router)
	handler = middlewares.WithRecovery(lg, handler)

	srv := graceful.NewServer(handler, cfg.GracefulTimeout, os.Interrupt)
	srv.Log = lg.Errorf
	srv.Addr = cfg.Addr
	return srv
}

type config struct {
	Addr            string
	LogLevel        string
	LogFormat       string
	StaticDir       string
	TemplateDir     string
	GracefulTimeout time.Duration
	MongoURI        string
}

func loadConfig() config {
	viper.SetDefault("MONGO_URI", "mongodb://localhost/droplets")
	viper.SetDefault("LOG_LEVEL", "debug")
	viper.SetDefault("LOG_FORMAT", "text")
	viper.SetDefault("ADDR", ":8080")
	viper.SetDefault("STATIC_DIR", "./web/static/")
	viper.SetDefault("TEMPLATE_DIR", "./web/templates/")
	viper.SetDefault("GRACEFUL_TIMEOUT", 20*time.Second)

	viper.ReadInConfig()
	viper.AutomaticEnv()

	return config{
		// application configuration
		Addr:            viper.GetString("ADDR"),
		StaticDir:       viper.GetString("STATIC_DIR"),
		TemplateDir:     viper.GetString("TEMPLATE_DIR"),
		LogLevel:        viper.GetString("LOG_LEVEL"),
		LogFormat:       viper.GetString("LOG_FORMAT"),
		GracefulTimeout: viper.GetDuration("GRACEFUL_TIMEOUT"),

		// store configuration
		MongoURI: viper.GetString("MONGO_URI"),
	}
}


================================================
FILE: pkg/doc.go
================================================
// Package pkg is the root for re-usable packages. This package should not
// contain any entities (exported or otherwise) since, a package named `pkg`
// does not express anything about its purpose and could become a catch all
// package like `utils`, `misc` etc. which should be avoided.
package pkg


================================================
FILE: pkg/errors/authorization.go
================================================
package errors

import "net/http"

// Common authorization related errors
const (
	TypeUnauthorized = "Unauthorized"
)

// Unauthorized can be used to generate an error that represents an unauthorized
// request.
func Unauthorized(reason string) error {
	return WithStack(&Error{
		Code:    http.StatusUnauthorized,
		Type:    TypeUnauthorized,
		Message: "You are not authorized to perform the requested action",
		Context: map[string]interface{}{
			"reason": reason,
		},
	})
}


================================================
FILE: pkg/errors/doc.go
================================================
// Package errors provides common error definitions and tools for dealing
// with errors. The API of this package is drop-in replacement for standard
// errors package except for one significant difference:
//   Since Error type used in this package embeds map inside, errors created
// by this package are not comparable and  hence cannot  be used to create
// Sentinel Errors.
package errors


================================================
FILE: pkg/errors/error.go
================================================
package errors

import (
	"fmt"
	"io"
)

// Error is a generic error representation with some fields to provide additional
// context around the error.
type Error struct {
	// Code can represent an http error code.
	Code int `json:"-"`

	// Type should be an error code to identify the error. Type and Context together
	// should provide enough context for robust error handling on client side.
	Type string `json:"type,omitempty"`

	// Context can contain additional information describing the error. Context will
	// be exposed only in API endpoints so that clients be integrated effectively.
	Context map[string]interface{} `json:"context,omitempty"`

	// Message should be a user-friendly error message which can be shown to the
	// end user without further modifications. However, clients are free to modify
	// this (e.g., for enabling localization), or augment this message with the
	// information available in the context before rendering a message to the end
	// user.
	Message string `json:"message,omitempty"`

	// original can contain an underlying error if any. This value will be returned
	// by the Cause() method.
	original error

	// stack will contain a minimal stack trace which can be used for logging and
	// debugging. stack should not be examined to handle errors.
	stack stack
}

// Cause returns the underlying error if any.
func (err Error) Cause() error {
	return err.original
}

func (err Error) Error() string {
	if origin := err.Cause(); origin != nil {
		return fmt.Sprintf("%s: %s: %s", origin, err.Type, err.Message)
	}

	return fmt.Sprintf("%s: %s", err.Type, err.Message)
}

// Format implements fmt.Formatter interface.
func (err Error) Format(st fmt.State, verb rune) {
	switch verb {
	case 'v':
		if st.Flag('+') {
			io.WriteString(st, err.Error())
			err.stack.Format(st, verb)
		} else {
			fmt.Fprintf(st, "%s: ", err.Type)
			for key, val := range err.Context {
				fmt.Fprintf(st, "%s='%s' ", key, val)
			}
		}
	case 's':
		io.WriteString(st, err.Error())
	case 'q':
		fmt.Fprintf(st, "%q", err.Error())
	}
}


================================================
FILE: pkg/errors/errors.go
================================================
package errors

import (
	"fmt"
	"net/http"
)

// TypeUnknown represents unknown error type.
const TypeUnknown = "Unknown"

// New returns an error object with formatted error message generated using
// the arguments.
func New(msg string, args ...interface{}) error {
	return &Error{
		Code:    http.StatusInternalServerError,
		Type:    TypeUnknown,
		Message: fmt.Sprintf(msg, args...),
		stack:   callStack(3),
	}
}

// Type attempts converting the err to Error type and extracts error Type.
// If conversion not possible, returns TypeUnknown.
func Type(err error) string {
	if e, ok := err.(*Error); ok {
		return e.Type
	}
	return TypeUnknown
}

// Wrapf wraps the given err with formatted message and returns a new error.
func Wrapf(err error, msg string, args ...interface{}) error {
	return WithStack(&Error{
		Code:     http.StatusInternalServerError,
		Type:     TypeUnknown,
		Message:  fmt.Sprintf(msg, args...),
		original: err,
	})
}

// WithStack annotates the given error with stack trace and returns the wrapped
// error.
func WithStack(err error) error {
	var wrappedErr Error
	if e, ok := err.(*Error); ok {
		wrappedErr = *e
	} else {
		wrappedErr.Type = TypeUnknown
		wrappedErr.Message = "Something went wrong"
		wrappedErr.original = err
	}

	wrappedErr.stack = callStack(3)
	return &wrappedErr
}

// Cause returns the underlying error if the given error is wrapping another error.
func Cause(err error) error {
	if err == nil {
		return nil
	}

	if e, ok := err.(*Error); ok {
		return e
	}

	return err
}


================================================
FILE: pkg/errors/resource.go
================================================
package errors

import "net/http"

// Common resource related error codes.
const (
	TypeResourceNotFound = "ResourceNotFound"
	TypeResourceConflict = "ResourceConflict"
)

// ResourceNotFound returns an error that represents an attempt to access a
// non-existent resource.
func ResourceNotFound(rType, rID string) error {
	return WithStack(&Error{
		Code:    http.StatusNotFound,
		Type:    TypeResourceNotFound,
		Message: "Resource you are requesting does not exist",
		Context: map[string]interface{}{
			"resource_type": rType,
			"resource_id":   rID,
		},
	})
}

// Conflict returns an error that represents a resource identifier conflict.
func Conflict(rType, rID string) error {
	return WithStack(&Error{
		Code:    http.StatusConflict,
		Type:    TypeResourceConflict,
		Message: "A resource with same name already exists",
		Context: map[string]interface{}{
			"resource_type": rType,
			"resource_id":   rID,
		},
	})
}


================================================
FILE: pkg/errors/stack.go
================================================
package errors

import (
	"fmt"
	"io"
	"path"
	"runtime"
)

const depth = 32

type stack []frame

// Format formats the stack of Frames according to the fmt.Formatter interface.
//
//    %s	lists source files for each Frame in the stack
//    %v	lists the source file and line number for each Frame in the stack
//
// Format accepts flags that alter the printing of some verbs, as follows:
//
//    %+v   Prints filename, function, and line number for each Frame in the stack.
func (st stack) Format(s fmt.State, verb rune) {
	switch verb {
	case 'v':
		switch {
		case s.Flag('+'):
			for _, f := range st {
				fmt.Fprintf(s, "\n%+v", f)
			}
		case s.Flag('#'):
			fmt.Fprintf(s, "%#v", []frame(st))
		default:
			fmt.Fprintf(s, "%v", []frame(st))
		}
	case 's':
		fmt.Fprintf(s, "%s", []frame(st))
	}
}

type frame struct {
	fn   string
	file string
	line int
}

// Format formats the frame according to the fmt.Formatter interface.
//
//    %s    source file
//    %d    source line
//    %n    function name
//    %v    equivalent to %s:%d
//
// Format accepts flags that alter the printing of some verbs, as follows:
//
//    %+s   function name and path of source file relative to the compile time
//          GOPATH separated by \n\t (<funcname>\n\t<path>)
//    %+v   equivalent to %+s:%d
func (f frame) Format(s fmt.State, verb rune) {
	switch verb {
	case 's':
		switch {
		case s.Flag('+'):
			fmt.Fprintf(s, "%s\n\t%s", f.fn, f.file)
		default:
			io.WriteString(s, path.Base(f.file))
		}
	case 'd':
		fmt.Fprintf(s, "%d", f.line)
	case 'n':
		io.WriteString(s, f.fn)
	case 'v':
		f.Format(s, 's')
		io.WriteString(s, ":")
		f.Format(s, 'd')
	}
}

func callStack(skip int) stack {
	var frames []frame
	var pcs [depth]uintptr

	n := runtime.Callers(skip, pcs[:])
	for i := 0; i < n; i++ {
		pc := pcs[i]
		frame := frameFromPC(pc)
		frames = append(frames, frame)
	}
	return stack(frames)
}

func frameFromPC(pc uintptr) frame {
	fr := frame{}

	fn := runtime.FuncForPC(pc)
	if fn == nil {
		fr.fn = "unknown"
	} else {
		fr.fn = fn.Name()
	}

	fr.file, fr.line = fn.FileLine(pc)
	return fr
}


================================================
FILE: pkg/errors/validation.go
================================================
package errors

import "net/http"

// Common validation error type codes.
const (
	TypeInvalidRequest = "InvalidRequest"
	TypeMissingField   = "MissingField"
	TypeInvalidValue   = "InvalidValue"
)

// Validation returns an error that can be used to represent an invalid request.
func Validation(reason string) error {
	return WithStack(&Error{
		Code:    http.StatusBadRequest,
		Type:    TypeInvalidRequest,
		Message: reason,
		Context: map[string]interface{}{},
	})
}

// InvalidValue can be used to generate an error that represents an invalid
// value for the 'field'. reason should be used to add detail describing why
// the value is invalid.
func InvalidValue(field string, reason string) error {
	return WithStack(&Error{
		Code:    http.StatusBadRequest,
		Type:    TypeInvalidValue,
		Message: "A parameter has invalid value",
		Context: map[string]interface{}{
			"field":  field,
			"reason": reason,
		},
	})
}

// MissingField can be used to generate an error that represents
// a empty value for a required field.
func MissingField(field string) error {
	return WithStack(&Error{
		Code:    http.StatusBadRequest,
		Type:    TypeMissingField,
		Message: "A required field is missing",
		Context: map[string]interface{}{
			"field": field,
		},
	})
}


================================================
FILE: pkg/graceful/doc.go
================================================
// Package graceful provides a simple wrapper for http.Handler which
// handles graceful shutdown based on registered signals. Server in
// this package closely follows the http.Server struct but can not be
// used as a drop-in replacement.
package graceful


================================================
FILE: pkg/graceful/graceful.go
================================================
package graceful

import (
	"context"
	"log"
	"net"
	"net/http"
	"os"
	"os/signal"
	"time"
)

// LogFunc can be set on the server to customize the message printed when the
// server is shutting down.
type LogFunc func(msg string, args ...interface{})

// NewServer creates a wrapper around the given handler.
func NewServer(handler http.Handler, timeout time.Duration, signals ...os.Signal) *Server {
	gss := &Server{}
	gss.server = &http.Server{Handler: handler}
	gss.signals = signals
	gss.Log = log.Printf
	gss.timeout = timeout
	return gss
}

// Server is a wrapper around an http handler. It provides methods
// to start the server with graceful-shutdown enabled.
type Server struct {
	Addr string
	Log  LogFunc

	server   *http.Server
	signals  []os.Signal
	timeout  time.Duration
	startErr error
}

// Serve starts the http listener with the registered http.Handler and
// then blocks until a interrupt signal is received.
func (gss *Server) Serve(l net.Listener) error {
	ctx, cancel := context.WithCancel(context.Background())
	go func() {
		if err := gss.server.Serve(l); err != nil {
			gss.startErr = err
			cancel()
		}
	}()
	return gss.waitForInterrupt(ctx)
}

// ServeTLS starts the http listener with the registered http.Handler and
// then blocks until a interrupt signal is received.
func (gss *Server) ServeTLS(l net.Listener, certFile, keyFile string) error {
	ctx, cancel := context.WithCancel(context.Background())
	go func() {
		if err := gss.server.ServeTLS(l, certFile, keyFile); err != nil {
			gss.startErr = err
			cancel()
		}
	}()
	return gss.waitForInterrupt(ctx)
}

// ListenAndServe serves the requests on a listener bound to interface
// specified by Addr
func (gss *Server) ListenAndServe() error {
	ctx, cancel := context.WithCancel(context.Background())
	go func() {
		gss.server.Addr = gss.Addr
		if err := gss.server.ListenAndServe(); err != http.ErrServerClosed {
			gss.startErr = err
			cancel()
		}
	}()
	return gss.waitForInterrupt(ctx)
}

// ListenAndServeTLS serves the requests on a listener bound to interface
// specified by Addr
func (gss *Server) ListenAndServeTLS(certFile, keyFile string) error {
	ctx, cancel := context.WithCancel(context.Background())
	go func() {
		if err := gss.server.ListenAndServeTLS(certFile, keyFile); err != http.ErrServerClosed {
			gss.startErr = err
			cancel()
		}
	}()
	return gss.waitForInterrupt(ctx)
}

func (gss *Server) waitForInterrupt(ctx context.Context) error {
	sigCh := make(chan os.Signal, 1)
	signal.Notify(sigCh, gss.signals...)

	select {
	case sig := <-sigCh:
		if gss.Log != nil {
			gss.Log("shutting down (signal=%s)...", sig)
		}
		break

	case <-ctx.Done():
		return gss.startErr
	}

	return gss.shutdown()
}

func (gss *Server) shutdown() error {
	ctx, cancel := context.WithTimeout(context.Background(), gss.timeout)
	defer cancel()
	return gss.server.Shutdown(ctx)
}


================================================
FILE: pkg/logger/doc.go
================================================
// Package logger provides logging functions. The loggers implemented in this
// package will have the API defined by the Logger interface. The interface
// is defined here (instead of where it is being used which is the right place),
// is because Logger interface is a common thing that gets used across the code
// base while being fairly constant in terms of its API.
package logger

// Logger implementation is responsible for providing structured and levled
// logging functions.
type Logger interface {
	Debugf(msg string, args ...interface{})
	Infof(msg string, args ...interface{})
	Warnf(msg string, args ...interface{})
	Errorf(msg string, args ...interface{})
	Fatalf(msg string, args ...interface{})

	// WithFields should return a logger which is annotated with the given
	// fields. These fields should be added to every logging call on the
	// returned logger.
	WithFields(m map[string]interface{}) Logger
}


================================================
FILE: pkg/logger/logrus.go
================================================
package logger

import (
	"io"
	"os"

	"github.com/sirupsen/logrus"
)

// New returns a logger implemented using the logrus package.
func New(wr io.Writer, level string, format string) Logger {
	if wr == nil {
		wr = os.Stderr
	}

	lr := logrus.New()
	lr.SetOutput(wr)
	lr.SetFormatter(&logrus.TextFormatter{})
	if format == "json" {
		lr.SetFormatter(&logrus.JSONFormatter{})
	}

	lvl, err := logrus.ParseLevel(level)
	if err != nil {
		lvl = logrus.WarnLevel
		lr.Warnf("failed to parse log-level '%s', defaulting to 'warning'", level)
	}
	lr.SetLevel(lvl)

	return &logrusLogger{
		Entry: logrus.NewEntry(lr),
	}
}

// logrusLogger provides functions for structured logging.
type logrusLogger struct {
	*logrus.Entry
}

func (ll *logrusLogger) WithFields(fields map[string]interface{}) Logger {
	annotatedEntry := ll.Entry.WithFields(logrus.Fields(fields))
	return &logrusLogger{
		Entry: annotatedEntry,
	}
}


================================================
FILE: pkg/middlewares/authn.go
================================================
package middlewares

import (
	"context"
	"net/http"

	"github.com/spy16/droplets/pkg/errors"
	"github.com/spy16/droplets/pkg/logger"
	"github.com/spy16/droplets/pkg/render"
)

var authUser = ctxKey("user")

// WithBasicAuth adds Basic authentication checks to the handler. Basic Auth header
// will be extracted from the request and verified using the verifier.
func WithBasicAuth(lg logger.Logger, next http.Handler, verifier UserVerifier) http.Handler {
	return http.HandlerFunc(func(wr http.ResponseWriter, req *http.Request) {
		name, secret, ok := req.BasicAuth()
		if !ok {
			render.JSON(wr, http.StatusUnauthorized, errors.Unauthorized("Basic auth header is not present"))
			return
		}

		verified := verifier.VerifySecret(req.Context(), name, secret)
		if !verified {
			wr.WriteHeader(http.StatusUnauthorized)
			render.JSON(wr, http.StatusUnauthorized, errors.Unauthorized("Invalid username or secret"))
			return
		}

		req = req.WithContext(context.WithValue(req.Context(), authUser, name))
		next.ServeHTTP(wr, req)
	})
}

// User extracts the username injected into the context by the auth middleware.
func User(req *http.Request) (string, bool) {
	val := req.Context().Value(authUser)
	if userName, ok := val.(string); ok {
		return userName, true
	}

	return "", false
}

type ctxKey string

// UserVerifier implementation is responsible for verifying the name-secret pair.
type UserVerifier interface {
	VerifySecret(ctx context.Context, name, secret string) bool
}

// UserVerifierFunc implements UserVerifier.
type UserVerifierFunc func(ctx context.Context, name, secret string) bool

// VerifySecret delegates call to the wrapped function.
func (uvf UserVerifierFunc) VerifySecret(ctx context.Context, name, secret string) bool {
	return uvf(ctx, name, secret)
}


================================================
FILE: pkg/middlewares/doc.go
================================================
// Package middlewares contains re-usable middleware functions. Middleware
// functions in this package follow standard http.HandlerFunc signature and
// hence are compatible with all standard http library functions.
package middlewares


================================================
FILE: pkg/middlewares/logging.go
================================================
package middlewares

import (
	"net/http"
	"time"

	"github.com/spy16/droplets/pkg/logger"
)

// WithRequestLogging adds logging to the given handler. Every request handled by
// 'next' will be logged with request information such as path, method, latency,
// client-ip, response status code etc. Logging will be done at info level only.
// Also, injects a logger into the ResponseWriter which can be later used by the
// handlers to perform additional logging.
func WithRequestLogging(logger logger.Logger, next http.Handler) http.Handler {
	return http.HandlerFunc(func(wr http.ResponseWriter, req *http.Request) {
		wrappedWr := wrap(wr, logger)

		start := time.Now()
		defer logRequest(logger, start, wrappedWr, req)

		next.ServeHTTP(wrappedWr, req)

	})
}

func logRequest(logger logger.Logger, startedAt time.Time, wr *wrappedWriter, req *http.Request) {
	duration := time.Now().Sub(startedAt)

	info := map[string]interface{}{
		"latency": duration,
		"status":  wr.wroteStatus,
	}

	logger.
		WithFields(requestInfo(req)).
		WithFields(info).
		Infof("request completed with code %d", wr.wroteStatus)
}


================================================
FILE: pkg/middlewares/recovery.go
================================================
package middlewares

import (
	"encoding/json"
	"net/http"

	"github.com/spy16/droplets/pkg/logger"
)

// WithRecovery recovers from any panics and logs them appropriately.
func WithRecovery(logger logger.Logger, next http.Handler) http.Handler {
	return http.HandlerFunc(func(wr http.ResponseWriter, req *http.Request) {
		ri := recoveryInfo{}
		safeHandler(next, &ri).ServeHTTP(wr, req)

		if ri.panicked {
			logger.Errorf("recovered from panic: %+v", ri.val)

			wr.WriteHeader(http.StatusInternalServerError)
			json.NewEncoder(wr).Encode(map[string]interface{}{
				"error": "Something went wrong",
			})
		}
	})
}

func safeHandler(next http.Handler, ri *recoveryInfo) http.Handler {
	return http.HandlerFunc(func(wr http.ResponseWriter, req *http.Request) {
		defer func() {
			if val := recover(); val != nil {
				ri.panicked = true
				ri.val = val
			}
		}()

		next.ServeHTTP(wr, req)
	})
}

type recoveryInfo struct {
	panicked bool
	val      interface{}
}


================================================
FILE: pkg/middlewares/utils.go
================================================
package middlewares

import (
	"net/http"

	"github.com/spy16/droplets/pkg/logger"
)

func requestInfo(req *http.Request) map[string]interface{} {
	return map[string]interface{}{
		"path":   req.URL.Path,
		"query":  req.URL.RawQuery,
		"method": req.Method,
		"client": req.RemoteAddr,
	}
}

func wrap(wr http.ResponseWriter, logger logger.Logger) *wrappedWriter {
	return &wrappedWriter{
		ResponseWriter: wr,
		Logger:         logger,
		wroteStatus:    http.StatusOK,
	}
}

type wrappedWriter struct {
	http.ResponseWriter
	logger.Logger

	wroteStatus int
}

func (wr *wrappedWriter) WriteHeader(statusCode int) {
	wr.wroteStatus = statusCode
	wr.ResponseWriter.WriteHeader(statusCode)
}


================================================
FILE: pkg/render/doc.go
================================================
// Package render provides simple and generic functions for rendering
// data structures using different encoding formats.
package render


================================================
FILE: pkg/render/render.go
================================================
package render

import (
	"encoding/json"
	"io"
	"net/http"
)

const contentTypeJSON = "application/json; charset=utf-8"

// JSON encodes the given val using the standard json package and writes
// the encoding output to the given writer. If the writer implements the
// http.ResponseWriter interface, then this function will also set the
// proper JSON content-type header with charset as UTF-8. Status will be
// considered only when wr is http.ResponseWriter and in that case, status
// must be a valid status code.
func JSON(wr io.Writer, status int, val interface{}) error {
	if hw, ok := wr.(http.ResponseWriter); ok {
		hw.Header().Set("Content-type", contentTypeJSON)
		hw.WriteHeader(status)
	}

	return json.NewEncoder(wr).Encode(val)
}


================================================
FILE: usecases/posts/doc.go
================================================
// Package posts has usecases around Post domain entity. This includes
// publishing and management of posts.
package posts


================================================
FILE: usecases/posts/publish.go
================================================
package posts

import (
	"context"
	"fmt"

	"github.com/spy16/droplets/domain"
	"github.com/spy16/droplets/pkg/errors"
	"github.com/spy16/droplets/pkg/logger"
)

// NewPublication initializes the publication usecase.
func NewPublication(lg logger.Logger, store Store, verifier userVerifier) *Publication {
	return &Publication{
		Logger:   lg,
		store:    store,
		verifier: verifier,
	}
}

// Publication implements the publishing usecases.
type Publication struct {
	logger.Logger

	store    Store
	verifier userVerifier
}

// Publish validates and persists the post into the store.
func (pub *Publication) Publish(ctx context.Context, post domain.Post) (*domain.Post, error) {
	if err := post.Validate(); err != nil {
		return nil, err
	}

	if !pub.verifier.Exists(ctx, post.Owner) {
		return nil, errors.Unauthorized(fmt.Sprintf("user '%s' not found", post.Owner))
	}

	if pub.store.Exists(ctx, post.Name) {
		return nil, errors.Conflict("Post", post.Name)
	}

	saved, err := pub.store.Save(ctx, post)
	if err != nil {
		pub.Warnf("failed to save post to the store: %+v", err)
	}

	return saved, nil
}

// Delete removes the post from the store.
func (pub *Publication) Delete(ctx context.Context, name string) (*domain.Post, error) {
	return pub.store.Delete(ctx, name)
}


================================================
FILE: usecases/posts/retrieval.go
================================================
package posts

import (
	"context"
	"errors"

	"github.com/spy16/droplets/domain"
	"github.com/spy16/droplets/pkg/logger"
)

// NewRetriever initializes the retrieval usecase with given store.
func NewRetriever(lg logger.Logger, store Store) *Retriever {
	return &Retriever{
		Logger: lg,
		store:  store,
	}
}

// Retriever provides retrieval related usecases.
type Retriever struct {
	logger.Logger

	store Store
}

// Get finds a post by its name.
func (ret *Retriever) Get(ctx context.Context, name string) (*domain.Post, error) {
	return ret.store.Get(ctx, name)
}

// Search finds all the posts matching the parameters in the query.
func (ret *Retriever) Search(ctx context.Context, query Query) ([]domain.Post, error) {
	return nil, errors.New("not implemented")
}

// Query represents parameters for executing a search. Zero valued fields
// in the query will be ignored.
type Query struct {
	Name  string   `json:"name,omitempty"`
	Owner string   `json:"owner,omitempty"`
	Tags  []string `json:"tags,omitempty"`
}


================================================
FILE: usecases/posts/store.go
================================================
package posts

import (
	"context"

	"github.com/spy16/droplets/domain"
)

// Store implementation is responsible for managing persistance of posts.
type Store interface {
	Get(ctx context.Context, name string) (*domain.Post, error)
	Exists(ctx context.Context, name string) bool
	Save(ctx context.Context, post domain.Post) (*domain.Post, error)
	Delete(ctx context.Context, name string) (*domain.Post, error)
}

// userVerifier is responsible for verifying existence of a user.
type userVerifier interface {
	Exists(ctx context.Context, name string) bool
}


================================================
FILE: usecases/users/doc.go
================================================
// Package users has usecases around User domain entity. This includes
// user registration, retrieval etc.
package users


================================================
FILE: usecases/users/registration.go
================================================
package users

import (
	"context"

	"github.com/spy16/droplets/domain"
	"github.com/spy16/droplets/pkg/errors"
	"github.com/spy16/droplets/pkg/logger"
)

// NewRegistrar initializes a Registration service object.
func NewRegistrar(lg logger.Logger, store Store) *Registrar {
	return &Registrar{
		Logger: lg,
		store:  store,
	}
}

// Registrar provides functions for user registration.
type Registrar struct {
	logger.Logger

	store Store
}

// Register creates a new user in the system using the given user object.
func (reg *Registrar) Register(ctx context.Context, user domain.User) (*domain.User, error) {
	if err := user.Validate(); err != nil {
		return nil, err
	}
	if len(user.Secret) < 8 {
		return nil, errors.InvalidValue("Secret", "secret must have 8 or more characters")
	}

	if reg.store.Exists(ctx, user.Name) {
		return nil, errors.Conflict("User", user.Name)
	}

	if err := user.HashSecret(); err != nil {
		return nil, err
	}

	saved, err := reg.store.Save(ctx, user)
	if err != nil {
		reg.Logger.Warnf("failed to save user object: %v", err)
		return nil, err
	}

	saved.Secret = ""
	return saved, nil
}


================================================
FILE: usecases/users/retrieval.go
================================================
package users

import (
	"context"

	"github.com/spy16/droplets/domain"
	"github.com/spy16/droplets/pkg/logger"
)

// NewRetriever initializes an instance of Retriever with given store.
func NewRetriever(lg logger.Logger, store Store) *Retriever {
	return &Retriever{
		Logger: lg,
		store:  store,
	}
}

// Retriever provides functions for retrieving user and user info.
type Retriever struct {
	logger.Logger

	store Store
}

// Search finds all users matching the tags.
func (ret *Retriever) Search(ctx context.Context, tags []string, limit int) ([]domain.User, error) {
	users, err := ret.store.FindAll(ctx, tags, limit)
	if err != nil {
		return nil, err
	}

	for i := range users {
		users[i].Secret = ""
	}

	return users, nil
}

// Get finds a user by name.
func (ret *Retriever) Get(ctx context.Context, name string) (*domain.User, error) {
	return ret.findUser(ctx, name, true)
}

// VerifySecret finds the user by name and verifies the secret against the has found
// in the store.
func (ret *Retriever) VerifySecret(ctx context.Context, name, secret string) bool {
	user, err := ret.findUser(ctx, name, false)
	if err != nil {
		return false
	}

	return user.CheckSecret(secret)
}

func (ret *Retriever) findUser(ctx context.Context, name string, stripSecret bool) (*domain.User, error) {
	user, err := ret.store.FindByName(ctx, name)
	if err != nil {
		ret.Debugf("failed to find user with name '%s': %v", name, err)
		return nil, err
	}

	if stripSecret {
		user.Secret = ""
	}
	return user, nil
}


================================================
FILE: usecases/users/store.go
================================================
package users

import (
	"context"

	"github.com/spy16/droplets/domain"
)

// Store implementation is responsible for managing persistence of
// users.
type Store interface {
	Exists(ctx context.Context, name string) bool
	Save(ctx context.Context, user domain.User) (*domain.User, error)
	FindByName(ctx context.Context, name string) (*domain.User, error)
	FindAll(ctx context.Context, tags []string, limit int) ([]domain.User, error)
}


================================================
FILE: web/static/main.css
================================================
html {
}

================================================
FILE: web/templates/index.tpl
================================================
<!doctype html>
<html lang="en">

<head>
  <!-- Required meta tags -->
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

  <!-- Bootstrap CSS -->
  <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO"
    crossorigin="anonymous">

  <title>Droplets</title>
</head>

<body>
  <div class="container">
    <nav class="navbar navbar-light bg-light">
       <a class="navbar-brand">
          <img src="static/favicon.ico" width="30" height="30" class="d-inline-block align-top" alt="">
          Droplets
       </a>
       <form class="form-inline" action="search" method="GET">
         <input class="form-control mr-sm-2" type="search" name="query" placeholder="Search" aria-label="Search">
         <button class="btn btn-outline-success my-2 my-sm-0" type="submit">Search</button>
       </form>
    </nav>

    <div class="container">
      <h1>Welcome </h1>
    </div>
  </div>
  <!-- Optional JavaScript -->
  <!-- jQuery first, then Popper.js, then Bootstrap JS -->
  <script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo"
    crossorigin="anonymous"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js" integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49"
    crossorigin="anonymous"></script>
  <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js" integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy"
    crossorigin="anonymous"></script>
</body>

</html>
Download .txt
gitextract_s7jwt71_/

├── .gitignore
├── CONTRIBUTING.md
├── Dockerfile
├── LICENSE
├── Makefile
├── README.md
├── docker-compose.yml
├── docs/
│   ├── interfaces.md
│   └── organization.md
├── domain/
│   ├── doc.go
│   ├── meta.go
│   ├── meta_test.go
│   ├── post.go
│   ├── post_test.go
│   ├── user.go
│   └── user_test.go
├── go.mod
├── go.sum
├── interfaces/
│   ├── mongo/
│   │   ├── doc.go
│   │   ├── mongo.go
│   │   ├── posts.go
│   │   └── users.go
│   ├── rest/
│   │   ├── doc.go
│   │   ├── posts.go
│   │   ├── rest.go
│   │   ├── users.go
│   │   └── utils.go
│   └── web/
│       ├── app.go
│       ├── doc.go
│       ├── fs.go
│       └── web.go
├── main.go
├── pkg/
│   ├── doc.go
│   ├── errors/
│   │   ├── authorization.go
│   │   ├── doc.go
│   │   ├── error.go
│   │   ├── errors.go
│   │   ├── resource.go
│   │   ├── stack.go
│   │   └── validation.go
│   ├── graceful/
│   │   ├── doc.go
│   │   └── graceful.go
│   ├── logger/
│   │   ├── doc.go
│   │   └── logrus.go
│   ├── middlewares/
│   │   ├── authn.go
│   │   ├── doc.go
│   │   ├── logging.go
│   │   ├── recovery.go
│   │   └── utils.go
│   └── render/
│       ├── doc.go
│       └── render.go
├── usecases/
│   ├── posts/
│   │   ├── doc.go
│   │   ├── publish.go
│   │   ├── retrieval.go
│   │   └── store.go
│   └── users/
│       ├── doc.go
│       ├── registration.go
│       ├── retrieval.go
│       └── store.go
└── web/
    ├── static/
    │   └── main.css
    └── templates/
        └── index.tpl
Download .txt
SYMBOL INDEX (151 symbols across 37 files)

FILE: domain/meta.go
  type Meta (line 11) | type Meta struct
    method SetDefaults (line 27) | func (meta *Meta) SetDefaults() {
    method Validate (line 35) | func (meta Meta) Validate() error {
  function empty (line 43) | func empty(str string) bool {

FILE: domain/meta_test.go
  function TestMeta_Validate (line 11) | func TestMeta_Validate(suite *testing.T) {
  function testValidation (line 28) | func testValidation(t *testing.T, validator validatable, expectErr bool,...
  type validatable (line 48) | type validatable interface

FILE: domain/post.go
  constant ContentLibrary (line 12) | ContentLibrary = "library"
  constant ContentLink (line 13) | ContentLink    = "link"
  constant ContentVideo (line 14) | ContentVideo   = "video"
  type Post (line 20) | type Post struct
    method Validate (line 36) | func (post Post) Validate() error {
  function contains (line 56) | func contains(val string, vals []string) bool {

FILE: domain/post_test.go
  function TestPost_Validate (line 10) | func TestPost_Validate(suite *testing.T) {

FILE: domain/user.go
  type User (line 11) | type User struct
    method Validate (line 22) | func (user User) Validate() error {
    method HashSecret (line 36) | func (user *User) HashSecret() error {
    method CheckSecret (line 46) | func (user User) CheckSecret(password string) bool {

FILE: domain/user_test.go
  function TestUser_CheckSecret (line 11) | func TestUser_CheckSecret(t *testing.T) {
  function TestUser_Validate (line 26) | func TestUser_Validate(suite *testing.T) {

FILE: interfaces/mongo/mongo.go
  function Connect (line 9) | func Connect(uri string, failFast bool) (*mgo.Database, func(), error) {
  function doNothing (line 24) | func doNothing() {}

FILE: interfaces/mongo/posts.go
  constant colPosts (line 13) | colPosts = "posts"
  function NewPostStore (line 16) | func NewPostStore(db *mgo.Database) *PostStore {
  type PostStore (line 23) | type PostStore struct
    method Exists (line 28) | func (posts *PostStore) Exists(ctx context.Context, name string) bool {
    method Get (line 39) | func (posts *PostStore) Get(ctx context.Context, name string) (*domain...
    method Save (line 55) | func (posts *PostStore) Save(ctx context.Context, post domain.Post) (*...
    method Delete (line 71) | func (posts *PostStore) Delete(ctx context.Context, name string) (*dom...

FILE: interfaces/mongo/users.go
  constant colUsers (line 13) | colUsers = "users"
  function NewUserStore (line 16) | func NewUserStore(db *mgo.Database) *UserStore {
  type UserStore (line 23) | type UserStore struct
    method Exists (line 29) | func (users *UserStore) Exists(ctx context.Context, name string) bool {
    method Save (line 40) | func (users *UserStore) Save(ctx context.Context, user domain.User) (*...
    method FindByName (line 56) | func (users *UserStore) FindByName(ctx context.Context, name string) (...
    method FindAll (line 72) | func (users *UserStore) FindAll(ctx context.Context, tags []string, li...
    method Delete (line 90) | func (users *UserStore) Delete(ctx context.Context, name string) (*dom...

FILE: interfaces/rest/posts.go
  function addPostsAPI (line 14) | func addPostsAPI(router *mux.Router, pub postPublication, ret postRetrie...
  type postController (line 26) | type postController struct
    method search (line 33) | func (pc *postController) search(wr http.ResponseWriter, req *http.Req...
    method get (line 43) | func (pc *postController) get(wr http.ResponseWriter, req *http.Reques...
    method post (line 54) | func (pc *postController) post(wr http.ResponseWriter, req *http.Reque...
    method delete (line 73) | func (pc *postController) delete(wr http.ResponseWriter, req *http.Req...
  type postRetriever (line 84) | type postRetriever interface
  type postPublication (line 89) | type postPublication interface

FILE: interfaces/rest/rest.go
  function New (line 14) | func New(logger logger.Logger, reg registration, ret retriever, postsRet...
  function notFoundHandler (line 27) | func notFoundHandler(wr http.ResponseWriter, req *http.Request) {
  function methodNotAllowedHandler (line 31) | func methodNotAllowedHandler(wr http.ResponseWriter, req *http.Request) {

FILE: interfaces/rest/users.go
  function addUsersAPI (line 12) | func addUsersAPI(router *mux.Router, reg registration, ret retriever, lo...
  type userController (line 24) | type userController struct
    method get (line 30) | func (uc *userController) get(wr http.ResponseWriter, req *http.Reques...
    method search (line 41) | func (uc *userController) search(wr http.ResponseWriter, req *http.Req...
    method post (line 52) | func (uc *userController) post(wr http.ResponseWriter, req *http.Reque...
  type registration (line 71) | type registration interface
  type retriever (line 75) | type retriever interface

FILE: interfaces/rest/utils.go
  function respond (line 11) | func respond(wr http.ResponseWriter, status int, v interface{}) {
  function respondErr (line 19) | func respondErr(wr http.ResponseWriter, err error) {
  function readRequest (line 27) | func readRequest(req *http.Request, v interface{}) error {
  type errorLogger (line 35) | type errorLogger interface

FILE: interfaces/web/app.go
  type app (line 8) | type app struct
    method indexHandler (line 13) | func (app app) indexHandler(wr http.ResponseWriter, req *http.Request) {

FILE: interfaces/web/fs.go
  function newSafeFileSystemServer (line 10) | func newSafeFileSystemServer(lg logger.Logger, root string) http.Handler {
  type safeFileSystem (line 20) | type safeFileSystem struct
    method Open (line 26) | func (sfs safeFileSystem) Open(path string) (http.File, error) {

FILE: interfaces/web/web.go
  function New (line 14) | func New(lg logger.Logger, cfg Config) (http.Handler, error) {
  type Config (line 41) | type Config struct
  function initTemplate (line 46) | func initTemplate(lg logger.Logger, name, path string) (*template.Templa...

FILE: main.go
  function main (line 21) | func main() {
  function setupServer (line 57) | func setupServer(cfg config, lg logger.Logger, web http.Handler, rest ht...
  type config (line 77) | type config struct
  function loadConfig (line 87) | func loadConfig() config {

FILE: pkg/errors/authorization.go
  constant TypeUnauthorized (line 7) | TypeUnauthorized = "Unauthorized"
  function Unauthorized (line 12) | func Unauthorized(reason string) error {

FILE: pkg/errors/error.go
  type Error (line 10) | type Error struct
    method Cause (line 39) | func (err Error) Cause() error {
    method Error (line 43) | func (err Error) Error() string {
    method Format (line 52) | func (err Error) Format(st fmt.State, verb rune) {

FILE: pkg/errors/errors.go
  constant TypeUnknown (line 9) | TypeUnknown = "Unknown"
  function New (line 13) | func New(msg string, args ...interface{}) error {
  function Type (line 24) | func Type(err error) string {
  function Wrapf (line 32) | func Wrapf(err error, msg string, args ...interface{}) error {
  function WithStack (line 43) | func WithStack(err error) error {
  function Cause (line 58) | func Cause(err error) error {

FILE: pkg/errors/resource.go
  constant TypeResourceNotFound (line 7) | TypeResourceNotFound = "ResourceNotFound"
  constant TypeResourceConflict (line 8) | TypeResourceConflict = "ResourceConflict"
  function ResourceNotFound (line 13) | func ResourceNotFound(rType, rID string) error {
  function Conflict (line 26) | func Conflict(rType, rID string) error {

FILE: pkg/errors/stack.go
  constant depth (line 10) | depth = 32
  type stack (line 12) | type stack
    method Format (line 22) | func (st stack) Format(s fmt.State, verb rune) {
  type frame (line 40) | type frame struct
    method Format (line 58) | func (f frame) Format(s fmt.State, verb rune) {
  function callStack (line 78) | func callStack(skip int) stack {
  function frameFromPC (line 91) | func frameFromPC(pc uintptr) frame {

FILE: pkg/errors/validation.go
  constant TypeInvalidRequest (line 7) | TypeInvalidRequest = "InvalidRequest"
  constant TypeMissingField (line 8) | TypeMissingField   = "MissingField"
  constant TypeInvalidValue (line 9) | TypeInvalidValue   = "InvalidValue"
  function Validation (line 13) | func Validation(reason string) error {
  function InvalidValue (line 25) | func InvalidValue(field string, reason string) error {
  function MissingField (line 39) | func MissingField(field string) error {

FILE: pkg/graceful/graceful.go
  type LogFunc (line 15) | type LogFunc
  function NewServer (line 18) | func NewServer(handler http.Handler, timeout time.Duration, signals ...o...
  type Server (line 29) | type Server struct
    method Serve (line 41) | func (gss *Server) Serve(l net.Listener) error {
    method ServeTLS (line 54) | func (gss *Server) ServeTLS(l net.Listener, certFile, keyFile string) ...
    method ListenAndServe (line 67) | func (gss *Server) ListenAndServe() error {
    method ListenAndServeTLS (line 81) | func (gss *Server) ListenAndServeTLS(certFile, keyFile string) error {
    method waitForInterrupt (line 92) | func (gss *Server) waitForInterrupt(ctx context.Context) error {
    method shutdown (line 110) | func (gss *Server) shutdown() error {

FILE: pkg/logger/doc.go
  type Logger (line 10) | type Logger interface

FILE: pkg/logger/logrus.go
  function New (line 11) | func New(wr io.Writer, level string, format string) Logger {
  type logrusLogger (line 36) | type logrusLogger struct
    method WithFields (line 40) | func (ll *logrusLogger) WithFields(fields map[string]interface{}) Logg...

FILE: pkg/middlewares/authn.go
  function WithBasicAuth (line 16) | func WithBasicAuth(lg logger.Logger, next http.Handler, verifier UserVer...
  function User (line 37) | func User(req *http.Request) (string, bool) {
  type ctxKey (line 46) | type ctxKey
  type UserVerifier (line 49) | type UserVerifier interface
  type UserVerifierFunc (line 54) | type UserVerifierFunc
    method VerifySecret (line 57) | func (uvf UserVerifierFunc) VerifySecret(ctx context.Context, name, se...

FILE: pkg/middlewares/logging.go
  function WithRequestLogging (line 15) | func WithRequestLogging(logger logger.Logger, next http.Handler) http.Ha...
  function logRequest (line 27) | func logRequest(logger logger.Logger, startedAt time.Time, wr *wrappedWr...

FILE: pkg/middlewares/recovery.go
  function WithRecovery (line 11) | func WithRecovery(logger logger.Logger, next http.Handler) http.Handler {
  function safeHandler (line 27) | func safeHandler(next http.Handler, ri *recoveryInfo) http.Handler {
  type recoveryInfo (line 40) | type recoveryInfo struct

FILE: pkg/middlewares/utils.go
  function requestInfo (line 9) | func requestInfo(req *http.Request) map[string]interface{} {
  function wrap (line 18) | func wrap(wr http.ResponseWriter, logger logger.Logger) *wrappedWriter {
  type wrappedWriter (line 26) | type wrappedWriter struct
    method WriteHeader (line 33) | func (wr *wrappedWriter) WriteHeader(statusCode int) {

FILE: pkg/render/render.go
  constant contentTypeJSON (line 9) | contentTypeJSON = "application/json; charset=utf-8"
  function JSON (line 17) | func JSON(wr io.Writer, status int, val interface{}) error {

FILE: usecases/posts/publish.go
  function NewPublication (line 13) | func NewPublication(lg logger.Logger, store Store, verifier userVerifier...
  type Publication (line 22) | type Publication struct
    method Publish (line 30) | func (pub *Publication) Publish(ctx context.Context, post domain.Post)...
    method Delete (line 52) | func (pub *Publication) Delete(ctx context.Context, name string) (*dom...

FILE: usecases/posts/retrieval.go
  function NewRetriever (line 12) | func NewRetriever(lg logger.Logger, store Store) *Retriever {
  type Retriever (line 20) | type Retriever struct
    method Get (line 27) | func (ret *Retriever) Get(ctx context.Context, name string) (*domain.P...
    method Search (line 32) | func (ret *Retriever) Search(ctx context.Context, query Query) ([]doma...
  type Query (line 38) | type Query struct

FILE: usecases/posts/store.go
  type Store (line 10) | type Store interface
  type userVerifier (line 18) | type userVerifier interface

FILE: usecases/users/registration.go
  function NewRegistrar (line 12) | func NewRegistrar(lg logger.Logger, store Store) *Registrar {
  type Registrar (line 20) | type Registrar struct
    method Register (line 27) | func (reg *Registrar) Register(ctx context.Context, user domain.User) ...

FILE: usecases/users/retrieval.go
  function NewRetriever (line 11) | func NewRetriever(lg logger.Logger, store Store) *Retriever {
  type Retriever (line 19) | type Retriever struct
    method Search (line 26) | func (ret *Retriever) Search(ctx context.Context, tags []string, limit...
    method Get (line 40) | func (ret *Retriever) Get(ctx context.Context, name string) (*domain.U...
    method VerifySecret (line 46) | func (ret *Retriever) VerifySecret(ctx context.Context, name, secret s...
    method findUser (line 55) | func (ret *Retriever) findUser(ctx context.Context, name string, strip...

FILE: usecases/users/store.go
  type Store (line 11) | type Store interface
Condensed preview — 61 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (80K chars).
[
  {
    "path": ".gitignore",
    "chars": 312,
    "preview": "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"
  },
  {
    "path": "CONTRIBUTING.md",
    "chars": 1497,
    "preview": "# Contributing to Droplets\n\nThanks for taking the time to contribute. You are Awesome! :heart:\n\nDroplets is built to sho"
  },
  {
    "path": "Dockerfile",
    "chars": 290,
    "preview": "FROM golang:1.11 as builder\nRUN mkdir /droplets-src\nWORKDIR /droplets-src\nCOPY ./ .\nRUN CGO_ENABLED=0 make setup all\n\nFR"
  },
  {
    "path": "LICENSE",
    "chars": 1073,
    "preview": "MIT License\n\nCopyright (c) 2018 Shivaprasad Bhat\n\nPermission is hereby granted, free of charge, to any person obtaining "
  },
  {
    "path": "Makefile",
    "chars": 447,
    "preview": "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\tve"
  },
  {
    "path": "README.md",
    "chars": 1431,
    "preview": "> WIP\n\n# droplets\n\n[![GoDoc](https://godoc.org/github.com/spy16/droplets?status.svg)](https://godoc.org/github.com/spy16"
  },
  {
    "path": "docker-compose.yml",
    "chars": 348,
    "preview": "version: '3.2'\n\nservices:\n  droplets:\n    build: ./\n    environment:\n      - MONGO_URI=mongodb://mongo\n      - LOG_LEVEL"
  },
  {
    "path": "docs/interfaces.md",
    "chars": 3779,
    "preview": "\n# Interfaces\n\nFollowing are some best practices for using interfaces:\n\n1. Define small interfaces with well defined sco"
  },
  {
    "path": "docs/organization.md",
    "chars": 3682,
    "preview": "\n# Directory Structure\n\nDirectory structure is based on [Clean Architecture](http://blog.cleancoder.com/uncle-bob/2012/0"
  },
  {
    "path": "domain/doc.go",
    "chars": 85,
    "preview": "// Package domain contains domain entities and core validation rules.\npackage domain\n"
  },
  {
    "path": "domain/meta.go",
    "chars": 1090,
    "preview": "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 abou"
  },
  {
    "path": "domain/meta_test.go",
    "chars": 944,
    "preview": "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/err"
  },
  {
    "path": "domain/post.go",
    "chars": 1396,
    "preview": "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"
  },
  {
    "path": "domain/post_test.go",
    "chars": 1288,
    "preview": "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 *te"
  },
  {
    "path": "domain/user.go",
    "chars": 1169,
    "preview": "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 r"
  },
  {
    "path": "domain/user_test.go",
    "chars": 1173,
    "preview": "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/err"
  },
  {
    "path": "go.mod",
    "chars": 765,
    "preview": "module github.com/spy16/droplets\n\ngo 1.15\n\nrequire (\n\tgithub.com/BurntSushi/toml v0.3.1 // indirect\n\tgithub.com/gorilla/"
  },
  {
    "path": "go.sum",
    "chars": 6064,
    "preview": "github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=\ngithub.com/BurntSushi/toml v0.3.1/go.m"
  },
  {
    "path": "interfaces/mongo/doc.go",
    "chars": 149,
    "preview": "// Package mongo contains any component in the entire project which interfaces\n// with MongoDB (e.g. different store imp"
  },
  {
    "path": "interfaces/mongo/mongo.go",
    "chars": 467,
    "preview": "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// d"
  },
  {
    "path": "interfaces/mongo/posts.go",
    "chars": 1997,
    "preview": "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"
  },
  {
    "path": "interfaces/mongo/users.go",
    "chars": 2603,
    "preview": "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"
  },
  {
    "path": "interfaces/rest/doc.go",
    "chars": 121,
    "preview": "// Package rest exposes the features of droplets as REST API. This\n// package uses gorilla/mux for routing.\npackage rest"
  },
  {
    "path": "interfaces/rest/posts.go",
    "chars": 2291,
    "preview": "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.co"
  },
  {
    "path": "interfaces/rest/rest.go",
    "chars": 1041,
    "preview": "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/spy1"
  },
  {
    "path": "interfaces/rest/users.go",
    "chars": 1958,
    "preview": "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.co"
  },
  {
    "path": "interfaces/rest/utils.go",
    "chars": 834,
    "preview": "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"
  },
  {
    "path": "interfaces/web/app.go",
    "chars": 277,
    "preview": "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,"
  },
  {
    "path": "interfaces/web/doc.go",
    "chars": 55,
    "preview": "// Package web contains MVC style web app.\npackage web\n"
  },
  {
    "path": "interfaces/web/fs.go",
    "chars": 829,
    "preview": "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 logg"
  },
  {
    "path": "interfaces/web/web.go",
    "chars": 1495,
    "preview": "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"
  },
  {
    "path": "main.go",
    "chars": 3177,
    "preview": "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\"githu"
  },
  {
    "path": "pkg/doc.go",
    "chars": 302,
    "preview": "// Package pkg is the root for re-usable packages. This package should not\n// contain any entities (exported or otherwis"
  },
  {
    "path": "pkg/errors/authorization.go",
    "chars": 481,
    "preview": "package errors\n\nimport \"net/http\"\n\n// Common authorization related errors\nconst (\n\tTypeUnauthorized = \"Unauthorized\"\n)\n\n"
  },
  {
    "path": "pkg/errors/doc.go",
    "chars": 394,
    "preview": "// Package errors provides common error definitions and tools for dealing\n// with errors. The API of this package is dro"
  },
  {
    "path": "pkg/errors/error.go",
    "chars": 2055,
    "preview": "package errors\n\nimport (\n\t\"fmt\"\n\t\"io\"\n)\n\n// Error is a generic error representation with some fields to provide addition"
  },
  {
    "path": "pkg/errors/errors.go",
    "chars": 1530,
    "preview": "package errors\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n)\n\n// TypeUnknown represents unknown error type.\nconst TypeUnknown = \"Unknow"
  },
  {
    "path": "pkg/errors/resource.go",
    "chars": 932,
    "preview": "package errors\n\nimport \"net/http\"\n\n// Common resource related error codes.\nconst (\n\tTypeResourceNotFound = \"ResourceNotF"
  },
  {
    "path": "pkg/errors/stack.go",
    "chars": 2106,
    "preview": "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 "
  },
  {
    "path": "pkg/errors/validation.go",
    "chars": 1266,
    "preview": "package errors\n\nimport \"net/http\"\n\n// Common validation error type codes.\nconst (\n\tTypeInvalidRequest = \"InvalidRequest\""
  },
  {
    "path": "pkg/graceful/doc.go",
    "chars": 258,
    "preview": "// Package graceful provides a simple wrapper for http.Handler which\n// handles graceful shutdown based on registered si"
  },
  {
    "path": "pkg/graceful/graceful.go",
    "chars": 2876,
    "preview": "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 t"
  },
  {
    "path": "pkg/logger/doc.go",
    "chars": 924,
    "preview": "// Package logger provides logging functions. The loggers implemented in this\n// package will have the API defined by th"
  },
  {
    "path": "pkg/logger/logrus.go",
    "chars": 913,
    "preview": "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 log"
  },
  {
    "path": "pkg/middlewares/authn.go",
    "chars": 1786,
    "preview": "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/droplet"
  },
  {
    "path": "pkg/middlewares/doc.go",
    "chars": 237,
    "preview": "// Package middlewares contains re-usable middleware functions. Middleware\n// functions in this package follow standard "
  },
  {
    "path": "pkg/middlewares/logging.go",
    "chars": 1113,
    "preview": "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"
  },
  {
    "path": "pkg/middlewares/recovery.go",
    "chars": 971,
    "preview": "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 r"
  },
  {
    "path": "pkg/middlewares/utils.go",
    "chars": 691,
    "preview": "package middlewares\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/spy16/droplets/pkg/logger\"\n)\n\nfunc requestInfo(req *http.Request"
  },
  {
    "path": "pkg/render/doc.go",
    "chars": 138,
    "preview": "// Package render provides simple and generic functions for rendering\n// data structures using different encoding format"
  },
  {
    "path": "pkg/render/render.go",
    "chars": 747,
    "preview": "package render\n\nimport (\n\t\"encoding/json\"\n\t\"io\"\n\t\"net/http\"\n)\n\nconst contentTypeJSON = \"application/json; charset=utf-8\""
  },
  {
    "path": "usecases/posts/doc.go",
    "chars": 124,
    "preview": "// Package posts has usecases around Post domain entity. This includes\n// publishing and management of posts.\npackage po"
  },
  {
    "path": "usecases/posts/publish.go",
    "chars": 1277,
    "preview": "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"
  },
  {
    "path": "usecases/posts/retrieval.go",
    "chars": 1023,
    "preview": "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"
  },
  {
    "path": "usecases/posts/store.go",
    "chars": 559,
    "preview": "package posts\n\nimport (\n\t\"context\"\n\n\t\"github.com/spy16/droplets/domain\"\n)\n\n// Store implementation is responsible for ma"
  },
  {
    "path": "usecases/users/doc.go",
    "chars": 122,
    "preview": "// Package users has usecases around User domain entity. This includes\n// user registration, retrieval etc.\npackage user"
  },
  {
    "path": "usecases/users/registration.go",
    "chars": 1125,
    "preview": "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"
  },
  {
    "path": "usecases/users/retrieval.go",
    "chars": 1512,
    "preview": "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// Ne"
  },
  {
    "path": "usecases/users/store.go",
    "chars": 438,
    "preview": "package users\n\nimport (\n\t\"context\"\n\n\t\"github.com/spy16/droplets/domain\"\n)\n\n// Store implementation is responsible for ma"
  },
  {
    "path": "web/static/main.css",
    "chars": 8,
    "preview": "html {\n}"
  },
  {
    "path": "web/templates/index.tpl",
    "chars": 1794,
    "preview": "<!doctype html>\n<html lang=\"en\">\n\n<head>\n  <!-- Required meta tags -->\n  <meta charset=\"utf-8\">\n  <meta name=\"viewport\" "
  }
]

About this extraction

This page contains the full source code of the spy16/droplets GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 61 files (70.1 KB), approximately 21.6k tokens, and a symbol index with 151 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!