<commit_before>package posts

import (
	"errors"
	"fmt"
	"log"
	"time"

	"github.com/boatilus/peppercorn/db"
	"github.com/boatilus/peppercorn/utility"
	"github.com/spf13/viper"
	rethink "gopkg.in/dancannon/gorethink.v2"
)

// Post contains all the information stored for a single post
type Post struct {
	ID      string    `gorethink:"id,omitempty"`
	Active  bool      `gorethink:"active"`
	Author  string    `gorethink:"user_id"`
	Content string    `gorethink:"content"`
	Time    time.Time `gorethink:"time"`
}

// Zip is a concatenation of a Post and a User. We return this from GetAndJoin
type Zip struct {
	ID         string    `gorethink:"id"`
	Active     bool      `gorethink:"active"`
	AuthorID   string    `gorethink:"user_id"`
	Content    string    `gorethink:"content"`
	Time       time.Time `gorethink:"time"`
	Avatar     string    `gorethink:"avatar"`
	AuthorName string    `gorethink:"name"`
	Title      string    `gorethink:"title"`
	Count      uint64
	PrettyTime string
}

// GetTable returns the name of the posts table from Viper.
func GetTable() string {
	return viper.GetString("db.posts_table")
}

// New fills and returns a Post object given an author and a post. The `Active` property
// is `true` by default, and `Time` is always `time.Now()`. RethinkDB will truncate .Time
// to millisecond precision.
func New(author string, content string) (*Post, error) {
	p := Post{
		// ID: RethinkDB will generate one for us on insert, so omit
		Active:  true,
		Author:  author,
		Content: content,
		Time:    time.Now(),
	}

	if !validate(&p) {
		return nil, errors.New("invalid data supplied")
	}

	return &p, nil
}

// Count returns the number of active posts as an int.
func Count() (int, error) {
	cursor, err := db.Get().Table(GetTable()).GetAllByIndex("active", true).Count().Run(db.Session)
	if err != nil {
		return 0, err
	}

	defer cursor.Close()

	var n int

	if err = cursor.One(&n); err != nil {
		return 0, err
	}

	return n, nil
}

// CountAll returns the total number of posts, including inactive posts, as an int.
func CountAll() (int, error) {
	cursor, err := db.Get().Table(GetTable()).Count().Run(db.Session)
	if err != nil {
		return 0, err
	}

	defer cursor.Close()

	var n int
	if err = cursor.One(&n); err != nil {
		return 0, err
	}

	return n, nil
}

// GetRange returns a range of posts specified by `first` -- the first post in the range --
// and `limit`, the number of posts.
func GetRange(first uint64, limit uint64) ([]Post, error) {
	// Don't let users try to load any page prior to the, uh, first one.
	if first < 1 {
		first = 1
	}

	// Always enforce a limit of 100
	if limit > 100 {
		limit = 100
	}

	// We want to order displayed posts by time posted (ascending), showing only active posts,
	// skipping all those we don't need and limiting the number of results to `limit`.
	cursor, err := db.Get().Table(GetTable()).GetAllByIndex("active", true).OrderBy(rethink.Asc("time")).Skip(first - 1).Limit(limit).Run(db.Session)
	if err != nil {
		return nil, err
	}

	defer cursor.Close()

	var posts []Post

	if err = cursor.All(&posts); err != nil {
		return nil, err
	}

	if len(posts) == 0 {
		return nil, errors.New("empty_result")
	}

	return posts, nil
}

func GetRangeJoined(first uint64, limit uint64) ([]Zip, error) {
	// Don't let users try to load any page prior to the, uh, first one.
	if first < 1 {
		first = 1
	}

	// Always enforce a limit of 100.
	if limit > 100 {
		limit = 100
	}

	// We want to order displayed posts by time posted (ascending), showing only active posts,
	// skipping inactive posts and limiting the number of results to `limit`.
	//
	// This requires some pretty insane query machinery to be efficient, which follows...
	table := db.Get().Table(GetTable())
	usersTable := db.Get().Table("users")
	btOpts := rethink.BetweenOpts{Index: "active_time"}
	min := []interface{}{true, rethink.MinVal}
	max := []interface{}{true, rethink.MaxVal}
	oOpts := rethink.OrderByOpts{Index: rethink.Asc("active_time")}
	eqjOpts := rethink.EqJoinOpts{Ordered: true}
	t := table.Between(min, max, btOpts).OrderBy(oOpts).Slice(first-1, limit)

	cursor, err := t.EqJoin("user_id", usersTable, eqjOpts).Zip().Run(db.Session)
	if err != nil {
		return nil, err
	}

	defer cursor.Close()

	if cursor.IsNil() {
		return nil, fmt.Errorf(`No posts found with first "%d" and limit "%d"`, first, limit)
	}

	var posts []Zip
	if err := cursor.All(&posts); err != nil {
		return nil, err
	}

	if len(posts) == 0 {
		return nil, errors.New("empty_result")
	}

	// Much to my dismay, the most performant way to get humanized timestamps is (probably) to handle
	// it here :( Since we're at it, we'll add each post number, as we have the data we need to do so.
	now := time.Now().UTC()

	for i := range posts {
		posts[i].Count = first + uint64(i)
		posts[i].PrettyTime = utility.FormatTime(posts[i].Time, now)
	}

	return posts, nil
}

// GetOne simply returns a single Post, given a post number.
func GetOne(n uint64) (*Post, error) {
	if n < 1 {
		return nil, errors.New("no_negative_allowed")
	}

	posts, err := GetRange(n, 1)
	if err != nil {
		return nil, err
	}

	return &posts[0], nil
}

// GetByID returns a single post given its ID.
func GetByID(id string) (*Post, error) {
	cursor, err := db.Get().Table(GetTable()).Get(id).Run(db.Session)
	if err != nil {
		return nil, err
	}

	defer cursor.Close()

	if cursor.IsNil() {
		return nil, fmt.Errorf("No post found at index %q", id)
	}

	var p Post
	if err = cursor.One(&p); err != nil {
		return nil, err
	}

	return &p, nil
}

// GetByIDJoined returns a  "zipped" struct containing post data and the merged user data for that
// post. Returns a nil `Zip` and an error on any failure.
func GetByIDJoined(id string) (*Zip, error) {
	log.Printf("Getting post with ID %q and joining with field %q", id, "user_id")

	cursor, err := db.Get().Table(GetTable()).GetAllByIndex("id", id).EqJoin("user_id", db.Get().Table("users")).Zip().Run(db.Session)
	if err != nil {
		return nil, err
	}

	defer cursor.Close()

	if cursor.IsNil() {
		return nil, fmt.Errorf("No post found with ID %q", id)
	}

	var z Zip
	err = cursor.One(&z)
	if err != nil {
		return nil, err
	}

	return &z, nil
}

// Edit accepts a post number and the content to update a post with. Errs if `n < 1` or if
// content length is 0, and for any database error.
func Edit(n uint64, newContent string) error {
	if n < 1 {
		return errors.New("Cannot attempt to edit post 0")
	}

	if len(newContent) == 0 {
		return errors.New("Post content length cannot be 0")
	}

	p, err := GetOne(n)
	if err != nil {
		return err
	}

	p.Content = newContent

	if err = update(p); err != nil {
		return err
	}

	return nil
}

// Submit accepts a complete Post and inserts it into the database, returning the ID a nil error
// on success, or an error on any failure.
func Submit(p *Post) (id string, err error) {
	if p != nil && !validate(p) {
		return "", errors.New("invalid Post supplied")
	}

	res, err := db.Get().Table(GetTable()).Insert(p).RunWrite(db.Session)
	if err != nil {
		return "", err
	}

	if res.Inserted == 0 {
		return "", fmt.Errorf("Failure in inserting post by user %q", p.Author)
	}

	log.Printf("Inserted post with ID %q", res.GeneratedKeys[0])

	return res.GeneratedKeys[0], nil
}

func Activate(id string) error {
	if err := updateStatus(id, true); err != nil {
		return err
	}

	return nil
}

func Deactivate(id string) error {
	if err := updateStatus(id, false); err != nil {
		return err
	}

	return nil
}

func validate(p *Post) bool {
	if len(p.Author) == 0 || len(p.Content) == 0 {
		return false
	}

	return true
}

func update(p *Post) error {
	io := rethink.InsertOpts{Conflict: "update"}

	if _, err := db.Get().Table(GetTable()).Insert(p, io).Run(db.Session); err != nil {
		return err
	}

	return nil
}

func updateStatus(id string, status bool) error {
	cursor, err := db.Get().Table(GetTable()).Get(id).Run(db.Session)
	if err != nil {
		return err
	}

	var p Post
	if err = cursor.One(&p); err != nil {
		return err
	}

	cursor.Close()

	p.Active = status

	io := rethink.InsertOpts{Conflict: "update"}

	if _, err = db.Get().Table(GetTable()).Insert(&p, io).Run(db.Session); err != nil {
		return err
	}

	return nil
}
<commit_msg>Better-document posts.GetRangeJoined<commit_after>package posts

import (
	"errors"
	"fmt"
	"log"
	"time"

	"github.com/boatilus/peppercorn/db"
	"github.com/boatilus/peppercorn/utility"
	"github.com/spf13/viper"
	rethink "gopkg.in/dancannon/gorethink.v2"
)

// Post contains all the information stored for a single post.
type Post struct {
	ID      string    `gorethink:"id,omitempty"`
	Active  bool      `gorethink:"active"`
	Author  string    `gorethink:"user_id"`
	Content string    `gorethink:"content"`
	Time    time.Time `gorethink:"time"`
}

// Zip is a concatenation of a Post and a User. We return this from GetAndJoin.
type Zip struct {
	ID         string    `gorethink:"id"`
	Active     bool      `gorethink:"active"`
	AuthorID   string    `gorethink:"user_id"`
	Content    string    `gorethink:"content"`
	Time       time.Time `gorethink:"time"`
	Avatar     string    `gorethink:"avatar"`
	AuthorName string    `gorethink:"name"`
	Title      string    `gorethink:"title"`
	Count      uint64
	PrettyTime string
}

// GetTable returns the name of the posts table from Viper.
func GetTable() string {
	return viper.GetString("db.posts_table")
}

// New fills and returns a Post object given an author and a post. The `Active` property
// is `true` by default, and `Time` is always `time.Now()`. RethinkDB will truncate .Time
// to millisecond precision.
func New(author string, content string) (*Post, error) {
	p := Post{
		// ID: RethinkDB will generate one for us on insert, so omit
		Active:  true,
		Author:  author,
		Content: content,
		Time:    time.Now(),
	}

	if !validate(&p) {
		return nil, errors.New("invalid data supplied")
	}

	return &p, nil
}

// Count returns the number of active posts as an int.
func Count() (int, error) {
	cursor, err := db.Get().Table(GetTable()).GetAllByIndex("active", true).Count().Run(db.Session)
	if err != nil {
		return 0, err
	}

	defer cursor.Close()

	var n int

	if err = cursor.One(&n); err != nil {
		return 0, err
	}

	return n, nil
}

// CountAll returns the total number of posts, including inactive posts, as an int.
func CountAll() (int, error) {
	cursor, err := db.Get().Table(GetTable()).Count().Run(db.Session)
	if err != nil {
		return 0, err
	}

	defer cursor.Close()

	var n int
	if err = cursor.One(&n); err != nil {
		return 0, err
	}

	return n, nil
}

// GetRange returns a range of posts specified by `first` -- the first post in the range --
// and `limit`, the number of posts.
func GetRange(first uint64, limit uint64) ([]Post, error) {
	// Don't let users try to load any page prior to the, uh, first one.
	if first < 1 {
		first = 1
	}

	// Always enforce a limit of 100
	if limit > 100 {
		limit = 100
	}

	// We want to order displayed posts by time posted (ascending), showing only active posts,
	// skipping all those we don't need and limiting the number of results to `limit`.
	cursor, err := db.Get().Table(GetTable()).GetAllByIndex("active", true).OrderBy(rethink.Asc("time")).Skip(first - 1).Limit(limit).Run(db.Session)
	if err != nil {
		return nil, err
	}

	defer cursor.Close()

	var posts []Post

	if err = cursor.All(&posts); err != nil {
		return nil, err
	}

	if len(posts) == 0 {
		return nil, errors.New("empty_result")
	}

	return posts, nil
}

func GetRangeJoined(first uint64, limit uint64) ([]Zip, error) {
	// Don't let users try to load any page prior to the, uh, first one.
	if first < 1 {
		first = 1
	}

	// Always enforce a limit of 100.
	if limit > 100 {
		limit = 100
	}

	// We want to order displayed posts by time posted (ascending), showing only active posts,
	// skipping inactive posts and limiting the number of results to `limit`.
	//
	// This requires some pretty insane query machinery to be efficient, which follows...
	table := db.Get().Table(GetTable())

	// We'll join against the `id` primary index against docs in the users table.
	usersTable := db.Get().Table("users")

	// For the Between term, we need to filter to all active posts (`true`) and between the minimum
	// date through the maximum possible date by querying against the `active_time` compound index.
	// We're required to specify MinVal and MaxVal here.
	btOpts := rethink.BetweenOpts{Index: "active_time"}
	min := []interface{}{true, rethink.MinVal}
	max := []interface{}{true, rethink.MaxVal}

	// We'll similarly order by the `active_time` index...
	oOpts := rethink.OrderByOpts{Index: rethink.Asc("active_time")}

	// EqJoin will negate the ordering specified by OrderBy unless we specify the `Ordered` option.
	eqjOpts := rethink.EqJoinOpts{Ordered: true}

	// The Slice term gives us effective pagination of results, but must be performed after filtering
	// and ordering.
	//
	// TODO: Consider whether moving the OrderBy term later in the chain can improve performance,
	// as per: https://www.rethinkdb.com/docs/optimization/
	t := table.Between(min, max, btOpts).OrderBy(oOpts).Slice(first-1, limit)

	// Zipping a user document into the post document without Excepting the user's ID field doesn't
	// trample over the post document's ID field, so we don't need to do anything else but run
	// the full query.
	cursor, err := t.EqJoin("user_id", usersTable, eqjOpts).Zip().Run(db.Session)
	if err != nil {
		return nil, err
	}

	defer cursor.Close()

	if cursor.IsNil() {
		return nil, fmt.Errorf(`No posts found with first "%d" and limit "%d"`, first, limit)
	}

	var posts []Zip
	if err := cursor.All(&posts); err != nil {
		return nil, err
	}

	if len(posts) == 0 {
		return nil, errors.New("empty_result")
	}

	// Much to my dismay, the most performant way to get humanized timestamps is (probably) to handle
	// it here :( Since we're at it, we'll add each post number, as we have the data we need to do so.
	now := time.Now().UTC()

	for i := range posts {
		posts[i].Count = first + uint64(i)
		posts[i].PrettyTime = utility.FormatTime(posts[i].Time, now)
	}

	return posts, nil
}

// GetOne simply returns a single Post, given a post number.
func GetOne(n uint64) (*Post, error) {
	if n < 1 {
		return nil, errors.New("no_negative_allowed")
	}

	posts, err := GetRange(n, 1)
	if err != nil {
		return nil, err
	}

	return &posts[0], nil
}

// GetByID returns a single post given its ID.
func GetByID(id string) (*Post, error) {
	cursor, err := db.Get().Table(GetTable()).Get(id).Run(db.Session)
	if err != nil {
		return nil, err
	}

	defer cursor.Close()

	if cursor.IsNil() {
		return nil, fmt.Errorf("No post found at index %q", id)
	}

	var p Post
	if err = cursor.One(&p); err != nil {
		return nil, err
	}

	return &p, nil
}

// GetByIDJoined returns a zipped struct containing post data and the merged user data for that
// post. Returns a nil `Zip` and an error on any failure.
func GetByIDJoined(id string) (*Zip, error) {
	log.Printf("Getting post with ID %q and joining with field %q", id, "user_id")

	cursor, err := db.Get().Table(GetTable()).GetAllByIndex("id", id).EqJoin("user_id", db.Get().Table("users")).Zip().Run(db.Session)
	if err != nil {
		return nil, err
	}

	defer cursor.Close()

	if cursor.IsNil() {
		return nil, fmt.Errorf("No post found with ID %q", id)
	}

	var z Zip
	err = cursor.One(&z)
	if err != nil {
		return nil, err
	}

	return &z, nil
}

// Edit accepts a post number and the content to update a post with. Errs if `n < 1` or if
// content length is 0, and for any database error.
func Edit(n uint64, newContent string) error {
	if n < 1 {
		return errors.New("Cannot attempt to edit post 0")
	}

	if len(newContent) == 0 {
		return errors.New("Post content length cannot be 0")
	}

	p, err := GetOne(n)
	if err != nil {
		return err
	}

	p.Content = newContent

	if err = update(p); err != nil {
		return err
	}

	return nil
}

// Submit accepts a complete Post and inserts it into the database, returning the ID a nil error
// on success, or an error on any failure.
func Submit(p *Post) (id string, err error) {
	if p != nil && !validate(p) {
		return "", errors.New("invalid Post supplied")
	}

	res, err := db.Get().Table(GetTable()).Insert(p).RunWrite(db.Session)
	if err != nil {
		return "", err
	}

	if res.Inserted == 0 {
		return "", fmt.Errorf("Failure in inserting post by user %q", p.Author)
	}

	log.Printf("Inserted post with ID %q", res.GeneratedKeys[0])

	return res.GeneratedKeys[0], nil
}

func Activate(id string) error {
	if err := updateStatus(id, true); err != nil {
		return err
	}

	return nil
}

func Deactivate(id string) error {
	if err := updateStatus(id, false); err != nil {
		return err
	}

	return nil
}

func validate(p *Post) bool {
	if len(p.Author) == 0 || len(p.Content) == 0 {
		return false
	}

	return true
}

func update(p *Post) error {
	io := rethink.InsertOpts{Conflict: "update"}

	if _, err := db.Get().Table(GetTable()).Insert(p, io).Run(db.Session); err != nil {
		return err
	}

	return nil
}

func updateStatus(id string, status bool) error {
	cursor, err := db.Get().Table(GetTable()).Get(id).Run(db.Session)
	if err != nil {
		return err
	}

	var p Post
	if err = cursor.One(&p); err != nil {
		return err
	}

	cursor.Close()

	p.Active = status

	io := rethink.InsertOpts{Conflict: "update"}

	if _, err = db.Get().Table(GetTable()).Insert(&p, io).Run(db.Session); err != nil {
		return err
	}

	return nil
}
<|endoftext|>


<commit_before>/*
   Nging is a toolbox for webmasters
   Copyright (C) 2018-present  Wenhui Shen <swh@admpub.com>

   This program is free software: you can redistribute it and/or modify
   it under the terms of the GNU Affero General Public License as published
   by the Free Software Foundation, either version 3 of the License, or
   (at your option) any later version.

   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU Affero General Public License for more details.

   You should have received a copy of the GNU Affero General Public License
   along with this program.  If not, see <https://www.gnu.org/licenses/>.
*/

package settings

import (
	"strings"

	"github.com/webx-top/db"
	"github.com/webx-top/echo"

	"github.com/admpub/log"
	"github.com/admpub/nging/application/dbschema"
)

var configDefaults = map[string]map[string]*dbschema.Config{
	`base`: {
		`apiKey`: {
			Key:         `apiKey`,
			Label:       `API密钥`,
			Description: ``,
			Value:       ``,
			Group:       `base`,
			Type:        `text`,
			Sort:        0,
			Disabled:    `N`,
		},
		`debug`: {
			Key:         `debug`,
			Label:       `调试模式`,
			Description: ``,
			Value:       `1`,
			Group:       `base`,
			Type:        `text`,
			Sort:        0,
			Disabled:    `N`,
		},
	},
	`smtp`: {
		`username`: {
			Key:         `username`,
			Label:       `登录名`,
			Description: ``,
			Value:       ``,
			Group:       `smtp`,
			Type:        `text`,
			Sort:        0,
			Disabled:    `N`,
		},
		`password`: {
			Key:         `password`,
			Label:       `密码`,
			Description: ``,
			Value:       ``,
			Group:       `smtp`,
			Type:        `text`,
			Sort:        0,
			Disabled:    `N`,
			Encrypted:   `Y`,
		},
		`host`: {
			Key:         `host`,
			Label:       `服务器`,
			Description: ``,
			Value:       `smtp.exmail.qq.com`,
			Group:       `smtp`,
			Type:        `text`,
			Sort:        0,
			Disabled:    `N`,
		},
		`port`: {
			Key:         `port`,
			Label:       `端口`,
			Description: ``,
			Value:       `465`,
			Group:       `smtp`,
			Type:        `text`,
			Sort:        0,
			Disabled:    `N`,
		},
		`secure`: {
			Key:         `secure`,
			Label:       `认证方式`,
			Description: ``,
			Value:       `SSL`,
			Group:       `smtp`,
			Type:        `text`,
			Sort:        0,
			Disabled:    `N`,
		},
		`identity`: {
			Key:         `identity`,
			Label:       `身份`,
			Description: ``,
			Value:       ``,
			Group:       `smtp`,
			Type:        `text`,
			Sort:        0,
			Disabled:    `N`,
		},
		`timeout`: {
			Key:         `timeout`,
			Label:       `超时时间`,
			Description: ``,
			Value:       ``,
			Group:       `smtp`,
			Type:        `text`,
			Sort:        0,
			Disabled:    `N`,
		},
		`engine`: {
			Key:         `engine`,
			Label:       `发送引擎`,
			Description: ``,
			Value:       `mail`,
			Group:       `smtp`,
			Type:        `text`,
			Sort:        0,
			Disabled:    `N`,
		},
		`from`: {
			Key:         `from`,
			Label:       `发信人地址`,
			Description: ``,
			Value:       ``,
			Group:       `smtp`,
			Type:        `text`,
			Sort:        0,
			Disabled:    `N`,
		},
		`queueSize`: {
			Key:         `queueSize`,
			Label:       `并发数量`,
			Description: ``,
			Value:       `10`,
			Group:       `smtp`,
			Type:        `text`,
			Sort:        0,
			Disabled:    `N`,
		},
	},
	`log`: {
		`saveFile`: {
			Key:         `saveFile`,
			Label:       `保存路径`,
			Description: ``,
			Value:       ``,
			Group:       `log`,
			Type:        `text`,
			Sort:        0,
			Disabled:    `N`,
		},
		`fileMaxBytes`: {
			Key:         `fileMaxBytes`,
			Label:       `日志文件尺寸`,
			Description: ``,
			Value:       ``,
			Group:       `log`,
			Type:        `text`,
			Sort:        0,
			Disabled:    `N`,
		},
		`targets`: {
			Key:         `targets`,
			Label:       `输出`,
			Description: ``,
			Value:       `console`,
			Group:       `log`,
			Type:        `list`,
			Sort:        0,
			Disabled:    `N`,
		},
		`colorable`: {
			Key:         `colorable`,
			Label:       `彩色日志`,
			Description: ``,
			Value:       `0`,
			Group:       `log`,
			Type:        `text`,
			Sort:        0,
			Disabled:    `N`,
		},
	},
}

func AddDefaultConfig(group string, configs map[string]*dbschema.Config) {
	if strings.Contains(group, `.`) {
		panic(`Group name is not allowed to contain ".": ` + group)
	}
	if _, y := configDefaults[group]; !y {
		configDefaults[group] = map[string]*dbschema.Config{}
	}
	for key, conf := range configs {
		if conf.Group != group {
			conf.Group = group
		}
		configDefaults[group][key] = conf
	}
}

func GetDefaultConfig(group string) map[string]*dbschema.Config {
	r, _ := configDefaults[group]
	return r
}

func GetDefaultConfigOk(group string) (map[string]*dbschema.Config, bool) {
	r, y := configDefaults[group]
	return r, y
}

func ConfigDefaultsAsStore() echo.H {
	return configAsStore(configDefaults)
}

func ConfigDefaults() map[string]map[string]*dbschema.Config {
	return configDefaults
}

func Init() error {
	log.Debug(`Initialize the configuration data in the database table`)
	m := &dbschema.Config{}
	_, err := m.ListByOffset(nil, func(r db.Result) db.Result {
		return r.Group(`group`)
	}, 0, -1)
	if err != nil {
		return err
	}
	existsList := m.Objects()
	existsIndex := map[string]int{}
	for index, row := range existsList {
		existsIndex[row.Group] = index
	}
	for _, setting := range Settings() {
		group := setting.Group
		if _, ok := existsIndex[group]; ok {
			continue
		}
		gs, ok := GetDefaultConfigOk(group)
		if !ok {
			continue
		}
		for _, conf := range gs {
			_, err = conf.EventOFF().Add()
			if err != nil {
				return err
			}
		}
	}
	return err
}

// ConfigAsStore {Group:{Key:ValueObject}}
func ConfigAsStore(groups ...string) echo.H {
	r := echo.H{}
	m := &dbschema.Config{}
	cond := db.NewCompounds()
	cond.Add(db.Cond{`disabled`: `N`})
	if len(groups) > 0 {
		if len(groups) > 1 {
			cond.Add(db.Cond{`group`: db.In(groups)})
		} else {
			cond.Add(db.Cond{`group`: groups[0]})
		}
	}
	m.ListByOffset(nil, nil, 0, -1, cond.And())
	for _, row := range m.Objects() {
		decoder := GetDecoder(row.Group)
		res, err := DecodeConfigValue(row, decoder)
		if err != nil {
			log.Error(err)
			continue
		}
		value := res.Get(`ValueObject`, row.Value)
		if _, y := r[row.Group]; !y {
			r[row.Group] = echo.H{row.Key: value}
		} else {
			r.Store(row.Group).Set(row.Key, value)
		}
	}
	return r
}

// {Group:{Key:ValueObject}}
func configAsStore(configList map[string]map[string]*dbschema.Config) echo.H {
	r := echo.H{}
	for group, configs := range configList {
		v := echo.H{}
		decoder := GetDecoder(group)
		for key, row := range configs {
			if row.Disabled == `Y` {
				continue
			}
			res, err := DecodeConfigValue(row, decoder)
			if err != nil {
				log.Error(err)
				continue
			}
			value := res.Get(`ValueObject`, row.Value)
			v.Set(key, value)
		}
		r.Set(group, v)
	}
	return r
}
<commit_msg>update<commit_after>/*
   Nging is a toolbox for webmasters
   Copyright (C) 2018-present  Wenhui Shen <swh@admpub.com>

   This program is free software: you can redistribute it and/or modify
   it under the terms of the GNU Affero General Public License as published
   by the Free Software Foundation, either version 3 of the License, or
   (at your option) any later version.

   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU Affero General Public License for more details.

   You should have received a copy of the GNU Affero General Public License
   along with this program.  If not, see <https://www.gnu.org/licenses/>.
*/

package settings

import (
	"strings"

	"github.com/webx-top/db"
	"github.com/webx-top/echo"

	"github.com/admpub/errors"
	"github.com/admpub/log"
	"github.com/admpub/nging/application/dbschema"
)

var configDefaults = map[string]map[string]*dbschema.Config{
	`base`: {
		`apiKey`: {
			Key:         `apiKey`,
			Label:       `API密钥`,
			Description: ``,
			Value:       ``,
			Group:       `base`,
			Type:        `text`,
			Sort:        0,
			Disabled:    `N`,
		},
		`debug`: {
			Key:         `debug`,
			Label:       `调试模式`,
			Description: ``,
			Value:       `1`,
			Group:       `base`,
			Type:        `text`,
			Sort:        0,
			Disabled:    `N`,
		},
	},
	`smtp`: {
		`username`: {
			Key:         `username`,
			Label:       `登录名`,
			Description: ``,
			Value:       ``,
			Group:       `smtp`,
			Type:        `text`,
			Sort:        0,
			Disabled:    `N`,
		},
		`password`: {
			Key:         `password`,
			Label:       `密码`,
			Description: ``,
			Value:       ``,
			Group:       `smtp`,
			Type:        `text`,
			Sort:        0,
			Disabled:    `N`,
			Encrypted:   `Y`,
		},
		`host`: {
			Key:         `host`,
			Label:       `服务器`,
			Description: ``,
			Value:       `smtp.exmail.qq.com`,
			Group:       `smtp`,
			Type:        `text`,
			Sort:        0,
			Disabled:    `N`,
		},
		`port`: {
			Key:         `port`,
			Label:       `端口`,
			Description: ``,
			Value:       `465`,
			Group:       `smtp`,
			Type:        `text`,
			Sort:        0,
			Disabled:    `N`,
		},
		`secure`: {
			Key:         `secure`,
			Label:       `认证方式`,
			Description: ``,
			Value:       `SSL`,
			Group:       `smtp`,
			Type:        `text`,
			Sort:        0,
			Disabled:    `N`,
		},
		`identity`: {
			Key:         `identity`,
			Label:       `身份`,
			Description: ``,
			Value:       ``,
			Group:       `smtp`,
			Type:        `text`,
			Sort:        0,
			Disabled:    `N`,
		},
		`timeout`: {
			Key:         `timeout`,
			Label:       `超时时间`,
			Description: ``,
			Value:       ``,
			Group:       `smtp`,
			Type:        `text`,
			Sort:        0,
			Disabled:    `N`,
		},
		`engine`: {
			Key:         `engine`,
			Label:       `发送引擎`,
			Description: ``,
			Value:       `mail`,
			Group:       `smtp`,
			Type:        `text`,
			Sort:        0,
			Disabled:    `N`,
		},
		`from`: {
			Key:         `from`,
			Label:       `发信人地址`,
			Description: ``,
			Value:       ``,
			Group:       `smtp`,
			Type:        `text`,
			Sort:        0,
			Disabled:    `N`,
		},
		`queueSize`: {
			Key:         `queueSize`,
			Label:       `并发数量`,
			Description: ``,
			Value:       `10`,
			Group:       `smtp`,
			Type:        `text`,
			Sort:        0,
			Disabled:    `N`,
		},
	},
	`log`: {
		`saveFile`: {
			Key:         `saveFile`,
			Label:       `保存路径`,
			Description: ``,
			Value:       ``,
			Group:       `log`,
			Type:        `text`,
			Sort:        0,
			Disabled:    `N`,
		},
		`fileMaxBytes`: {
			Key:         `fileMaxBytes`,
			Label:       `日志文件尺寸`,
			Description: ``,
			Value:       ``,
			Group:       `log`,
			Type:        `text`,
			Sort:        0,
			Disabled:    `N`,
		},
		`targets`: {
			Key:         `targets`,
			Label:       `输出`,
			Description: ``,
			Value:       `console`,
			Group:       `log`,
			Type:        `list`,
			Sort:        0,
			Disabled:    `N`,
		},
		`colorable`: {
			Key:         `colorable`,
			Label:       `彩色日志`,
			Description: ``,
			Value:       `0`,
			Group:       `log`,
			Type:        `text`,
			Sort:        0,
			Disabled:    `N`,
		},
	},
}

func AddDefaultConfig(group string, configs map[string]*dbschema.Config) {
	if strings.Contains(group, `.`) {
		panic(`Group name is not allowed to contain ".": ` + group)
	}
	if _, y := configDefaults[group]; !y {
		configDefaults[group] = map[string]*dbschema.Config{}
	}
	for key, conf := range configs {
		if conf.Group != group {
			conf.Group = group
		}
		configDefaults[group][key] = conf
	}
}

func GetDefaultConfig(group string) map[string]*dbschema.Config {
	r, _ := configDefaults[group]
	return r
}

func GetDefaultConfigOk(group string) (map[string]*dbschema.Config, bool) {
	r, y := configDefaults[group]
	return r, y
}

func ConfigDefaultsAsStore() echo.H {
	return configAsStore(configDefaults)
}

func ConfigDefaults() map[string]map[string]*dbschema.Config {
	return configDefaults
}

func Init() error {
	log.Debug(`Initialize the configuration data in the database table`)
	m := &dbschema.Config{}
	_, err := m.ListByOffset(nil, func(r db.Result) db.Result {
		return r.Group(`group`)
	}, 0, -1)
	if err != nil {
		return err
	}
	existsList := m.Objects()
	existsIndex := map[string]int{}
	for index, row := range existsList {
		existsIndex[row.Group] = index
	}
	for _, setting := range Settings() {
		group := setting.Group
		if _, ok := existsIndex[group]; ok {
			continue
		}
		gs, ok := GetDefaultConfigOk(group)
		if !ok {
			continue
		}
		for _, conf := range gs {
			_, err = conf.EventOFF().Add()
			if err != nil {
				return errors.WithMessage(err, `Add configuration data`)
			}
		}
	}
	return err
}

// ConfigAsStore {Group:{Key:ValueObject}}
func ConfigAsStore(groups ...string) echo.H {
	r := echo.H{}
	m := &dbschema.Config{}
	cond := db.NewCompounds()
	cond.Add(db.Cond{`disabled`: `N`})
	if len(groups) > 0 {
		if len(groups) > 1 {
			cond.Add(db.Cond{`group`: db.In(groups)})
		} else {
			cond.Add(db.Cond{`group`: groups[0]})
		}
	}
	m.ListByOffset(nil, nil, 0, -1, cond.And())
	for _, row := range m.Objects() {
		decoder := GetDecoder(row.Group)
		res, err := DecodeConfigValue(row, decoder)
		if err != nil {
			log.Error(`Parsing system settings "`+row.Group+`.`+row.Key, `": `, err)
			continue
		}
		value := res.Get(`ValueObject`, row.Value)
		if _, y := r[row.Group]; !y {
			r[row.Group] = echo.H{row.Key: value}
		} else {
			r.Store(row.Group).Set(row.Key, value)
		}
	}
	return r
}

// {Group:{Key:ValueObject}}
func configAsStore(configList map[string]map[string]*dbschema.Config) echo.H {
	r := echo.H{}
	for group, configs := range configList {
		v := echo.H{}
		decoder := GetDecoder(group)
		for key, row := range configs {
			if row.Disabled == `Y` {
				continue
			}
			res, err := DecodeConfigValue(row, decoder)
			if err != nil {
				log.Error(`Parsing system settings "`+group+`.`+key, `": `, err)
				continue
			}
			value := res.Get(`ValueObject`, row.Value)
			v.Set(key, value)
		}
		r.Set(group, v)
	}
	return r
}
<|endoftext|>


<commit_before>package dokugen

import (
	"testing"
)

func TestCellCreation(t *testing.T) {
	cell := NewCell(nil, 0, 0, "1")
	if cell.Number != 1 {
		t.Log("Number came back wrong")
		t.Fail()
	}
	if cell.Row != 0 {
		t.Log("Row came back wrong")
		t.Fail()
	}
	if cell.Col != 0 {
		t.Log("Cell came back wrong")
		t.Fail()
	}
	//TODO: test failing for values that are too high.
}

func TestBasic(t *testing.T) {
	_ = Grid{}
}
<commit_msg>TESTS FAIL. Added test for grid creation, which uncovers an error somewhere.<commit_after>package dokugen

import (
	"strings"
	"testing"
)

func TestCellCreation(t *testing.T) {
	cell := NewCell(nil, 0, 0, "1")
	if cell.Number != 1 {
		t.Log("Number came back wrong")
		t.Fail()
	}
	if cell.Row != 0 {
		t.Log("Row came back wrong")
		t.Fail()
	}
	if cell.Col != 0 {
		t.Log("Cell came back wrong")
		t.Fail()
	}
	//TODO: test failing for values that are too high.
}

func TestGridCreation(t *testing.T) {
	cellData := "1"
	rowData := strings.Join(nCopies(cellData, DIM), COL_SEP)
	data := strings.Join(nCopies(rowData, DIM), ROW_SEP)
	grid := NewGrid(data)
	if len(grid.cells) != DIM*DIM {
		t.Log("Didn't generate enough cells")
		t.Fail()
	}
	if grid.cells[10].Number != 1 {
		t.Log("A random spot check of a cell had the wrong number: %s", grid.cells[10])
		t.Fail()
	}
}

func nCopies(in string, copies int) (result []string) {
	for i := 0; i < copies; i++ {
		result = append(result, in)
	}
	return
}
<|endoftext|>


<commit_before>package pi

import (
	"encoding/json"
	"encoding/xml"
	"fmt"
	"io"
	"io/ioutil"
	"mime/multipart"
	"net/http"
)

var (
	errNoFiles = fmt.Errorf("no files")
)

// J is an helper to write JSON.
// Example:
//		c.WriteJSON(pi.J{"status": "OK"})
//
type J map[string]interface{}

// RequestContext represents the context of the HTTP request.
// It is shared across interceptors and handler.
type RequestContext struct {
	W        http.ResponseWriter
	R        *http.Request
	RouteURL string
	Data     map[interface{}]interface{}
}

// newRequestContext returns a new RequestContext.
func newRequestContext(w http.ResponseWriter, r *http.Request, routeURL string) *RequestContext {
	return &RequestContext{
		W:        w,
		R:        r,
		RouteURL: routeURL,
		Data:     make(map[interface{}]interface{}),
	}
}

// WriteString writes the specified strings to the ResponseWriter.
func (c *RequestContext) WriteString(strings ...string) error {
	for _, s := range strings {
		if _, err := c.W.Write([]byte(s)); err != nil {
			return err
		}
	}
	return nil
}

// WriteJSON marshal the object to JSON and writes it via the ResponseWriter.
func (c *RequestContext) WriteJSON(object interface{}) error {
	c.W.Header().Set("Content-Type", "application/json; charset=utf-8")
	if debugMode {
		output, err := json.MarshalIndent(object, "", "  ")
		if err != nil {
			return err
		}
		writeDebug("WriteJSON", c.R.RemoteAddr, string(output))
		c.W.Write(output)
	} else {
		output, err := json.Marshal(object)
		if err != nil {
			return err
		}
		c.W.Write(output)
	}
	return nil
}

// WriteXML marshal the object to XML and writes it via the ResponseWriter.
func (c *RequestContext) WriteXML(object interface{}) error {
	c.W.Header().Set("Content-Type", "application/xml; charset=utf-8")
	if debugMode {
		output, err := xml.MarshalIndent(object, "", "  ")
		if err != nil {
			return err
		}
		writeDebug("WriteXML", c.R.RemoteAddr, string(output))
		c.W.Write(output)
	} else {
		output, err := xml.Marshal(object)
		if err != nil {
			return err
		}
		c.W.Write(output)
	}
	return nil
}

// SetStatusCode sets the response status code.
func (c *RequestContext) SetStatusCode(statusCode int)  {
	c.W.WriteHeader(statusCode)
}

// GetBody return the body as a ReadCloser. It is the client responsibility to close the body.
func (c *RequestContext) GetBody() io.ReadCloser {
	return c.R.Body
}

// GetRawBody returns the body as a byte array, closing the body reader.
func (c *RequestContext) GetRawBody() ([]byte, error) {
	body := c.GetBody()
	defer body.Close()
	rawBody, err := ioutil.ReadAll(body)
	if err != nil {
		return nil, err
	}
	if debugMode {
		writeDebug("GetRawBody", c.R.RemoteAddr, fmt.Sprintf("got %s", string(rawBody)))
	}
	return ioutil.ReadAll(body)
}

// GetJSONObject call json.Unmarshal by sending the reference of the given object.
// For example:
//		func GetUser(c *pi.RequestContext) error {
// 			user := &User{}
// 			if err := c.GetJSONObject(user); err != nil {
//				return pi.NewError(400, err)
//			}
//			// Do something with the user...
//			return nil
//		}
//
func (c *RequestContext) GetJSONObject(object interface{}) error {
	rawBody, err := c.GetRawBody()
	if err != nil {
		return err
	}
	return json.Unmarshal(rawBody, &object)
}

// GetXMLObject call xml.Unmarshal by sending the reference of the given object.
func (c *RequestContext) GetXMLObject(object interface{}) error {
	rawBody, err := c.GetRawBody()
	if err != nil {
		return err
	}
	return xml.Unmarshal(rawBody, &object)
}

// GetURLParam returns an URL parameter.
// For example, given this URL:
//		/user?id=1234
//
//		fmt.Println(c.GetURLParam("id"))
//		// Outputs 1234
//
func (c *RequestContext) GetURLParam(param string) string {
	return c.R.FormValue(param)
}

// GetRouteVariable returns a route variable.
// For example:
//		getUserByID := func(c *RequestContext) error {
//			id := c.GetRouteVariable("id")
//			// Do something with the ID.
//			return nil
//		}
//
//		p := pi.New()
//		p.Route("/user/{id}").Get(getUserByID)
//		p.ListenAndServe(":8080")
//
func (c *RequestContext) GetRouteVariable(key string) string {
	return c.R.URL.Query().Get(":" + key)
}

// GetFileHeaders returns an array of FileHeader.
// For example:
//		getImages := func(c *RequestContext) error {
//			files, err := c.GetFileHeaders("files")
//			if err != nil {
//				return pi.NewError(400, err)
//			}
//			// Handle files
//			return nil
//		}
//
//		p := pi.New()
//		p.Route("/images").Post(getImages)
//		p.ListenAndServe(":8080")
//
func (c *RequestContext) GetFileHeaders(key string) ([]*multipart.FileHeader, error) {
	if err := c.R.ParseMultipartForm(32 << 20); err != nil {
		return nil, err
	}
	if c.R.MultipartForm != nil && c.R.MultipartForm.File[key] != nil {
		if debugMode {
			writeDebug("GetFileHeaders", c.R.RemoteAddr, fmt.Sprintf("got %d files from key %s", len(c.R.MultipartForm.File[key]), key))
		}
		return c.R.MultipartForm.File[key], nil
	}
	if debugMode {
		writeDebug("GetFileHeaders", c.R.RemoteAddr, fmt.Sprintf("got 0 files from key %s", key))
	}
	return nil, errNoFiles
}
<commit_msg>Added header handling functions on request context<commit_after>package pi

import (
	"encoding/json"
	"encoding/xml"
	"fmt"
	"io"
	"io/ioutil"
	"mime/multipart"
	"net/http"
)

var (
	errNoFiles = fmt.Errorf("no files")
)

// J is an helper to write JSON.
// Example:
//		c.WriteJSON(pi.J{"status": "OK"})
//
type J map[string]interface{}

// RequestContext represents the context of the HTTP request.
// It is shared across interceptors and handler.
type RequestContext struct {
	W        http.ResponseWriter
	R        *http.Request
	RouteURL string
	Data     map[interface{}]interface{}
}

// newRequestContext returns a new RequestContext.
func newRequestContext(w http.ResponseWriter, r *http.Request, routeURL string) *RequestContext {
	return &RequestContext{
		W:        w,
		R:        r,
		RouteURL: routeURL,
		Data:     make(map[interface{}]interface{}),
	}
}

// WriteString writes the specified strings to the ResponseWriter.
func (c *RequestContext) WriteString(strings ...string) error {
	for _, s := range strings {
		if _, err := c.W.Write([]byte(s)); err != nil {
			return err
		}
	}
	return nil
}

// WriteJSON marshal the object to JSON and writes it via the ResponseWriter.
func (c *RequestContext) WriteJSON(object interface{}) error {
	c.W.Header().Set("Content-Type", "application/json; charset=utf-8")
	if debugMode {
		output, err := json.MarshalIndent(object, "", "  ")
		if err != nil {
			return err
		}
		writeDebug("WriteJSON", c.R.RemoteAddr, string(output))
		c.W.Write(output)
	} else {
		output, err := json.Marshal(object)
		if err != nil {
			return err
		}
		c.W.Write(output)
	}
	return nil
}

// WriteXML marshal the object to XML and writes it via the ResponseWriter.
func (c *RequestContext) WriteXML(object interface{}) error {
	c.W.Header().Set("Content-Type", "application/xml; charset=utf-8")
	if debugMode {
		output, err := xml.MarshalIndent(object, "", "  ")
		if err != nil {
			return err
		}
		writeDebug("WriteXML", c.R.RemoteAddr, string(output))
		c.W.Write(output)
	} else {
		output, err := xml.Marshal(object)
		if err != nil {
			return err
		}
		c.W.Write(output)
	}
	return nil
}

// SetStatusCode sets the response status code.
func (c *RequestContext) SetStatusCode(statusCode int)  {
	c.W.WriteHeader(statusCode)
}

// GetBody return the body as a ReadCloser. It is the client responsibility to close the body.
func (c *RequestContext) GetBody() io.ReadCloser {
	return c.R.Body
}

// GetRawBody returns the body as a byte array, closing the body reader.
func (c *RequestContext) GetRawBody() ([]byte, error) {
	body := c.GetBody()
	defer body.Close()
	rawBody, err := ioutil.ReadAll(body)
	if err != nil {
		return nil, err
	}
	if debugMode {
		writeDebug("GetRawBody", c.R.RemoteAddr, fmt.Sprintf("got %s", string(rawBody)))
	}
	return ioutil.ReadAll(body)
}

// GetJSONObject call json.Unmarshal by sending the reference of the given object.
// For example:
//		func GetUser(c *pi.RequestContext) error {
// 			user := &User{}
// 			if err := c.GetJSONObject(user); err != nil {
//				return pi.NewError(400, err)
//			}
//			// Do something with the user...
//			return nil
//		}
//
func (c *RequestContext) GetJSONObject(object interface{}) error {
	rawBody, err := c.GetRawBody()
	if err != nil {
		return err
	}
	return json.Unmarshal(rawBody, &object)
}

// GetXMLObject call xml.Unmarshal by sending the reference of the given object.
func (c *RequestContext) GetXMLObject(object interface{}) error {
	rawBody, err := c.GetRawBody()
	if err != nil {
		return err
	}
	return xml.Unmarshal(rawBody, &object)
}

// GetURLParam returns an URL parameter.
// For example, given this URL:
//		/user?id=1234
//
//		fmt.Println(c.GetURLParam("id"))
//		// Outputs 1234
//
func (c *RequestContext) GetURLParam(param string) string {
	return c.R.FormValue(param)
}

// GetRouteVariable returns a route variable.
// For example:
//		getUserByID := func(c *RequestContext) error {
//			id := c.GetRouteVariable("id")
//			// Do something with the ID.
//			return nil
//		}
//
//		p := pi.New()
//		p.Route("/user/{id}").Get(getUserByID)
//		p.ListenAndServe(":8080")
//
func (c *RequestContext) GetRouteVariable(key string) string {
	return c.R.URL.Query().Get(":" + key)
}

// GetFileHeaders returns an array of FileHeader.
// For example:
//		getImages := func(c *RequestContext) error {
//			files, err := c.GetFileHeaders("files")
//			if err != nil {
//				return pi.NewError(400, err)
//			}
//			// Handle files
//			return nil
//		}
//
//		p := pi.New()
//		p.Route("/images").Post(getImages)
//		p.ListenAndServe(":8080")
//
func (c *RequestContext) GetFileHeaders(key string) ([]*multipart.FileHeader, error) {
	if err := c.R.ParseMultipartForm(32 << 20); err != nil {
		return nil, err
	}
	if c.R.MultipartForm != nil && c.R.MultipartForm.File[key] != nil {
		if debugMode {
			writeDebug("GetFileHeaders", c.R.RemoteAddr, fmt.Sprintf("got %d files from key %s", len(c.R.MultipartForm.File[key]), key))
		}
		return c.R.MultipartForm.File[key], nil
	}
	if debugMode {
		writeDebug("GetFileHeaders", c.R.RemoteAddr, fmt.Sprintf("got 0 files from key %s", key))
	}
	return nil, errNoFiles
}

// GetHeader returns the first value of the header associated with the given key.
func (c *RequestContext) GetHeader(key string) string {
	return c.R.Header.Get(key)
}

// SetHeader sets the header entries associated with key to
// the single element value.  It replaces any existing
// values associated with key.
func (c *RequestContext) SetHeader(key, value string) {
	c.R.Header.Set(key, value)
}

// AddHeader adds the key, value pair to the header.
// It appends to any existing values associated with key.
func (c *RequestContext) AddHeader(key, value string) {
	c.R.Header.Add(key, value)
}

// DeleteHeader deletes the values associated with key.
func (c *RequestContext) DeleteHeader(key string) {
	c.R.Header.Del(key)
}

// GetHeaders returns the array of values associated with the key.
func (c *RequestContext) GetHeaders(key string) []string {
	return c.R.Header[key]
}
<|endoftext|>


<commit_before>package namespaces

/*
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <linux/sched.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>

static const kBufSize = 256;

void get_args(int *argc, char ***argv) {
	// Read argv
	int fd = open("/proc/self/cmdline", O_RDONLY);

	// Read the whole commandline.
	ssize_t contents_size = 0;
	ssize_t contents_offset = 0;
	char *contents = NULL;
	ssize_t bytes_read = 0;
	do {
		contents_size += kBufSize;
		contents = (char *) realloc(contents, contents_size);
		bytes_read = read(fd, contents + contents_offset, contents_size - contents_offset);
		contents_offset += bytes_read;
	} while (bytes_read > 0);
	close(fd);

	// Parse the commandline into an argv. /proc/self/cmdline has \0 delimited args.
	ssize_t i;
	*argc = 0;
	for (i = 0; i < contents_offset; i++) {
		if (contents[i] == '\0') {
			(*argc)++;
		}
	}
	*argv = (char **) malloc(sizeof(char *) * ((*argc) + 1));
	int idx;
	for (idx = 0; idx < (*argc); idx++) {
		(*argv)[idx] = contents;
		contents += strlen(contents) + 1;
	}
	(*argv)[*argc] = NULL;
}

void nsenter() {
	int argc;
	char **argv;
	get_args(&argc, &argv);

	// Ignore if this is not for us.
	if (argc < 2 || strcmp(argv[1], "nsenter") != 0) {
		return;
	}

	// USAGE: <binary> nsenter <PID> <process label> <container JSON> <argv>...
	if (argc < 6) {
		fprintf(stderr, "nsenter: Incorrect usage, not enough arguments\n");
		exit(1);
	}
	pid_t init_pid = strtol(argv[2], NULL, 10);
	if (errno != 0 || init_pid <= 0) {
		fprintf(stderr, "nsenter: Failed to parse PID from \"%s\" with error: \"%s\"\n", argv[2], strerror(errno));
		exit(1);
	}
	argc -= 3;
	argv += 3;

	// Setns on all supported namespaces.
	char ns_dir[kBufSize];
	memset(ns_dir, 0, kBufSize);
	if (snprintf(ns_dir, kBufSize - 1, "/proc/%d/ns/", init_pid) < 0) {
		fprintf(stderr, "nsenter: Error getting ns dir path with error: \"%s\"\n", strerror(errno));
		exit(1);
	}
	struct dirent *dent;
	DIR *dir = opendir(ns_dir);
	if (dir == NULL) {
		fprintf(stderr, "nsenter: Failed to open directory \"%s\" with error: \"%s\"\n", ns_dir, strerror(errno));
		exit(1);
	}

	while((dent = readdir(dir)) != NULL) {
		if(strcmp(dent->d_name, ".") == 0 || strcmp(dent->d_name, "..") == 0 || strcmp(dent->d_name, "user") == 0) {
			continue;
		}

		// Get and open the namespace for the init we are joining..
		char buf[kBufSize];
		memset(buf, 0, kBufSize);
		strncat(buf, ns_dir, kBufSize - 1);
		strncat(buf, dent->d_name, kBufSize - 1);
		int fd = open(buf, O_RDONLY);
		if (fd == -1) {
			fprintf(stderr, "nsenter: Failed to open ns file \"%s\" for ns \"%s\" with error: \"%s\"\n", buf, dent->d_name, strerror(errno));
			exit(1);
		}

		// Set the namespace.
		if (setns(fd, 0) == -1) {
			fprintf(stderr, "nsenter: Failed to setns for \"%s\" with error: \"%s\"\n", dent->d_name, strerror(errno));
			exit(1);
		}
		close(fd);
	}
	closedir(dir);

	// We must fork to actually enter the PID namespace.
	int child = fork();
	if (child == 0) {
		// Finish executing, let the Go runtime take over.
		return;
	} else {
		// Parent, wait for the child.
		int status = 0;
		if (waitpid(child, &status, 0) == -1) {
			fprintf(stderr, "nsenter: Failed to waitpid with error: \"%s\"\n", strerror(errno));
			exit(1);
		}

		// Forward the child's exit code or re-send its death signal.
		if (WIFEXITED(status)) {
			exit(WEXITSTATUS(status));
		} else if (WIFSIGNALED(status)) {
			kill(getpid(), WTERMSIG(status));
		}
		exit(1);
	}

	return;
}

__attribute__((constructor)) init() {
	nsenter();
}
*/
import "C"
<commit_msg>nsenter: fix setns() for rhel6 (glibc-2.12)<commit_after>package namespaces

/*
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <linux/sched.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>

static const kBufSize = 256;

void get_args(int *argc, char ***argv) {
	// Read argv
	int fd = open("/proc/self/cmdline", O_RDONLY);

	// Read the whole commandline.
	ssize_t contents_size = 0;
	ssize_t contents_offset = 0;
	char *contents = NULL;
	ssize_t bytes_read = 0;
	do {
		contents_size += kBufSize;
		contents = (char *) realloc(contents, contents_size);
		bytes_read = read(fd, contents + contents_offset, contents_size - contents_offset);
		contents_offset += bytes_read;
	} while (bytes_read > 0);
	close(fd);

	// Parse the commandline into an argv. /proc/self/cmdline has \0 delimited args.
	ssize_t i;
	*argc = 0;
	for (i = 0; i < contents_offset; i++) {
		if (contents[i] == '\0') {
			(*argc)++;
		}
	}
	*argv = (char **) malloc(sizeof(char *) * ((*argc) + 1));
	int idx;
	for (idx = 0; idx < (*argc); idx++) {
		(*argv)[idx] = contents;
		contents += strlen(contents) + 1;
	}
	(*argv)[*argc] = NULL;
}

// Use raw setns syscall for versions of glibc that don't include it (namely glibc-2.12)
#if __GLIBC__ == 2 && __GLIBC_MINOR__ < 14 && defined(SYS_setns)
#define _GNU_SOURCE
#include <sched.h>
#include "syscall.h"
int setns(int fd, int nstype) {
  return syscall(SYS_setns, fd, nstype);
}
#endif

void nsenter() {
	int argc;
	char **argv;
	get_args(&argc, &argv);

	// Ignore if this is not for us.
	if (argc < 2 || strcmp(argv[1], "nsenter") != 0) {
		return;
	}

	// USAGE: <binary> nsenter <PID> <process label> <container JSON> <argv>...
	if (argc < 6) {
		fprintf(stderr, "nsenter: Incorrect usage, not enough arguments\n");
		exit(1);
	}
	pid_t init_pid = strtol(argv[2], NULL, 10);
	if (errno != 0 || init_pid <= 0) {
		fprintf(stderr, "nsenter: Failed to parse PID from \"%s\" with error: \"%s\"\n", argv[2], strerror(errno));
		exit(1);
	}
	argc -= 3;
	argv += 3;

	// Setns on all supported namespaces.
	char ns_dir[kBufSize];
	memset(ns_dir, 0, kBufSize);
	if (snprintf(ns_dir, kBufSize - 1, "/proc/%d/ns/", init_pid) < 0) {
		fprintf(stderr, "nsenter: Error getting ns dir path with error: \"%s\"\n", strerror(errno));
		exit(1);
	}
	struct dirent *dent;
	DIR *dir = opendir(ns_dir);
	if (dir == NULL) {
		fprintf(stderr, "nsenter: Failed to open directory \"%s\" with error: \"%s\"\n", ns_dir, strerror(errno));
		exit(1);
	}

	while((dent = readdir(dir)) != NULL) {
		if(strcmp(dent->d_name, ".") == 0 || strcmp(dent->d_name, "..") == 0 || strcmp(dent->d_name, "user") == 0) {
			continue;
		}

		// Get and open the namespace for the init we are joining..
		char buf[kBufSize];
		memset(buf, 0, kBufSize);
		strncat(buf, ns_dir, kBufSize - 1);
		strncat(buf, dent->d_name, kBufSize - 1);
		int fd = open(buf, O_RDONLY);
		if (fd == -1) {
			fprintf(stderr, "nsenter: Failed to open ns file \"%s\" for ns \"%s\" with error: \"%s\"\n", buf, dent->d_name, strerror(errno));
			exit(1);
		}

		// Set the namespace.
		if (setns(fd, 0) == -1) {
			fprintf(stderr, "nsenter: Failed to setns for \"%s\" with error: \"%s\"\n", dent->d_name, strerror(errno));
			exit(1);
		}
		close(fd);
	}
	closedir(dir);

	// We must fork to actually enter the PID namespace.
	int child = fork();
	if (child == 0) {
		// Finish executing, let the Go runtime take over.
		return;
	} else {
		// Parent, wait for the child.
		int status = 0;
		if (waitpid(child, &status, 0) == -1) {
			fprintf(stderr, "nsenter: Failed to waitpid with error: \"%s\"\n", strerror(errno));
			exit(1);
		}

		// Forward the child's exit code or re-send its death signal.
		if (WIFEXITED(status)) {
			exit(WEXITSTATUS(status));
		} else if (WIFSIGNALED(status)) {
			kill(getpid(), WTERMSIG(status));
		}
		exit(1);
	}

	return;
}

__attribute__((constructor)) init() {
	nsenter();
}
*/
import "C"
<|endoftext|>


<commit_before>// Copyright 2015 Daniel Pupius

package rcache

import (
	"container/list"
	"sync"
)

type lru struct {
	maxSizeBytes int64
	delegate     Cache
	mu           sync.Mutex
	elementMap   map[CacheKey]*list.Element
	elementList  *list.List // least recently used at the front.
}

// NewLRU returns a cache with a max byte size. Least recently used entries will
// be evicted first.
func NewLRU(name string, maxSizeBytes int64) Cache {
	return &lru{
		maxSizeBytes: maxSizeBytes,
		delegate:     New(name),
		elementMap:   make(map[CacheKey]*list.Element),
		elementList:  list.New(),
	}
}

func (l *lru) RegisterFetcher(fn interface{}) {
	l.delegate.RegisterFetcher(fn)
}

func (l *lru) Entries() []CacheEntry {
	// TODO(dan): Return copy that reflects LRU ordering.
	return l.delegate.Entries()
}

func (l *lru) Size() int64 {
	return l.delegate.Size()
}

func (l *lru) Invalidate(key CacheKey, recursive bool) bool {
	// TODO(dan): recursive invalidation of LRU doesn't work.
	if ok := l.delegate.Invalidate(key, false); ok {
		l.elementList.Remove(l.elementMap[key])
		delete(l.elementMap, key)
		return true
	}
	return false
}

func (l *lru) Peek(key CacheKey) bool {
	return l.delegate.Peek(key)
}

func (l *lru) GetCacheEntry(key CacheKey) *CacheEntry {
	return l.delegate.GetCacheEntry(key)
}

func (l *lru) Get(key CacheKey) ([]byte, error) {
	e := l.GetCacheEntry(key)

	if e.Error == nil {
		l.mu.Lock()
		if element, ok := l.elementMap[e.Key]; ok {
			l.elementList.MoveToBack(element)
		} else {
			l.elementMap[key] = l.elementList.PushBack(key)
		}

		// Remove least recently used elements until cache is under capacity.
		for l.delegate.Size() > l.maxSizeBytes {
			f := l.elementList.Front()
			key := l.elementList.Remove(f).(CacheKey)
			delete(l.elementMap, key)
			l.delegate.Invalidate(key, false)
		}
		l.mu.Unlock()
	}

	return e.Bytes, e.Error
}
<commit_msg>Lock on Invalidation<commit_after>// Copyright 2015 Daniel Pupius

package rcache

import (
	"container/list"
	"sync"
)

type lru struct {
	maxSizeBytes int64
	delegate     Cache
	mu           sync.Mutex
	elementMap   map[CacheKey]*list.Element
	elementList  *list.List // least recently used at the front.
}

// NewLRU returns a cache with a max byte size. Least recently used entries will
// be evicted first.
func NewLRU(name string, maxSizeBytes int64) Cache {
	return &lru{
		maxSizeBytes: maxSizeBytes,
		delegate:     New(name),
		elementMap:   make(map[CacheKey]*list.Element),
		elementList:  list.New(),
	}
}

func (l *lru) RegisterFetcher(fn interface{}) {
	l.delegate.RegisterFetcher(fn)
}

func (l *lru) Entries() []CacheEntry {
	// TODO(dan): Return copy that reflects LRU ordering.
	return l.delegate.Entries()
}

func (l *lru) Size() int64 {
	return l.delegate.Size()
}

func (l *lru) Invalidate(key CacheKey, recursive bool) bool {
	l.mu.Lock()
	defer l.mu.Unlock()
	// TODO(dan): recursive invalidation of LRU doesn't work.
	if ok := l.delegate.Invalidate(key, false); ok {
		l.elementList.Remove(l.elementMap[key])
		delete(l.elementMap, key)
		return true
	}
	return false
}

func (l *lru) Peek(key CacheKey) bool {
	return l.delegate.Peek(key)
}

func (l *lru) GetCacheEntry(key CacheKey) *CacheEntry {
	return l.delegate.GetCacheEntry(key)
}

func (l *lru) Get(key CacheKey) ([]byte, error) {
	e := l.GetCacheEntry(key)

	if e.Error == nil {
		l.mu.Lock()
		if element, ok := l.elementMap[e.Key]; ok {
			l.elementList.MoveToBack(element)
		} else {
			l.elementMap[key] = l.elementList.PushBack(key)
		}

		// Remove least recently used elements until cache is under capacity.
		for l.delegate.Size() > l.maxSizeBytes {
			f := l.elementList.Front()
			key := l.elementList.Remove(f).(CacheKey)
			delete(l.elementMap, key)
			l.delegate.Invalidate(key, false)
		}
		l.mu.Unlock()
	}

	return e.Bytes, e.Error
}
<|endoftext|>


<commit_before>package main

import (
	"encoding/json"
	"fmt"
	"strings"

	"gopkg.in/jackc/pgx.v2"

	"github.com/golang/groupcache"

	"github.com/LeKovr/dbrpc/workman"
	"github.com/LeKovr/go-base/logger"
)

// TableRow holds query result row
type TableRow map[string]interface{}

// TableRows holds slice of query result rows
type TableRows []TableRow

// -----------------------------------------------------------------------------

// Processor gets value from cache and converts it into Result struct
func cacheFetcher(log *logger.Log, cacheGroup *groupcache.Group) workman.WorkerFunc {
	// https://github.com/capotej/groupcache-db-experiment
	return func(payload string) workman.Result {
		var data []byte
		log.Debugf("asked for %s from groupcache", payload)
		err := cacheGroup.Get(nil, payload,
			groupcache.AllocatingByteSliceSink(&data))
		var res workman.Result
		if err != nil {
			res = workman.Result{Success: false, Error: err.Error()}
		} else if len(data) == 0 {
			res = workman.Result{Success: false, Error: "This internal error must be catched earlier. Please contact vendor"}
		} else {
			d := data[1:]
			raw := json.RawMessage(d)
			ok := data[0] == 1
			if ok { // First byte stores success state (1: true, 0: false)
				res = workman.Result{Success: ok, Result: &raw}
			} else {
				res = workman.Result{Success: ok, Error: &raw}
			}
		}
		return res
	}
}

// -----------------------------------------------------------------------------

func dbFetcher(cfg *AplFlags, log *logger.Log, db *pgx.ConnPool) groupcache.GetterFunc {
	return func(ctx groupcache.Context, key string, dest groupcache.Sink) error {
		log.Printf("asking for %s from dbserver", key)

		var isOk byte = 1 // status: success

		data, err := dbQuery(cfg, log, db, key)
		if err != nil {
			log.Warnf("Query for key %s error: %+v", key, err)
			isOk = 0
			data, _ = json.Marshal(err.Error())
		}

		result := []byte{isOk}
		result = append(result, data...)

		dd := result[1:]
		log.Debugf("Save data: %s", dd)
		dest.SetBytes([]byte(result))
		return nil
	}
}

// -----------------------------------------------------------------------------

// FuncDef holds function definition
type FuncDef struct {
	NspName string  // function namespace
	ProName string  // function name
	Permit  *string // permit code
	MaxAge  int     // cache max age
}

// FuncMap holds map of function definitions
type FuncMap map[string]FuncDef

// -----------------------------------------------------------------------------

func indexFetcher(cfg *AplFlags, log *logger.Log, db *pgx.ConnPool) (index *FuncMap, err error) {
	var rows *pgx.Rows

	q := fmt.Sprintf("select code, nspname, proname, permit_code, max_age from %s()", cfg.ArgIndexFunc)
	log.Debugf("Query: %s", q)

	rows, err = db.Query(q)
	if err != nil {
		return
	}

	ind := FuncMap{}
	for rows.Next() {
		var fmr FuncDef
		var code string
		err = rows.Scan(&code, &fmr.NspName, &fmr.ProName, &fmr.Permit, &fmr.MaxAge)
		if err != nil {
			log.Warnf("Value fetch error: %s", err.Error())
			return
		}
		ind[code] = fmr
	}
	// Any errors encountered by rows.Next or rows.Scan will be returned here
	if rows.Err() != nil {
		err = rows.Err()
		return
	}
	log.Debugf("Index: %+v", ind)
	index = &ind
	return
}

// -----------------------------------------------------------------------------

func dbQuery(cfg *AplFlags, log *logger.Log, db *pgx.ConnPool, key string) (data []byte, err error) {
	var rows *pgx.Rows
	var q string
	var vals []interface{}

	if strings.HasPrefix(key, "[") {
		var args []interface{}
		err = json.Unmarshal([]byte(key), &args)
		if err != nil {
			return
		}
		q, vals = PrepareFuncSQL(cfg, args)
	} else {
		var args CallDef
		err = json.Unmarshal([]byte(key), &args)
		if err != nil {
			return
		}
		q, vals = PrepareFuncSQLmap(cfg, args)
	}
	log.Debugf("Query: %s args: %+v", q, vals)
	rows, err = db.Query(q, vals...)
	if err != nil {
		return
	}

	table, err := FetchSQLResult(rows, log)
	defer rows.Close()
	if err != nil {
		return
	}

	data, err = json.Marshal(*table)
	if err != nil {
		return
	}

	return
}

// -----------------------------------------------------------------------------

// PrepareFuncSQL prepares sql query with args placeholders
func PrepareFuncSQL(cfg *AplFlags, args []interface{}) (string, []interface{}) {
	proc := args[1].(string)
	argVals := args[2:]

	argValPrep := make([]interface{}, len(argVals))
	argIDs := make([]string, len(argVals))

	for i, v := range argVals {
		argIDs[i] = fmt.Sprintf("$%d", i+1)
		argValPrep[i] = v
	}

	argIDStr := strings.Join(argIDs, ",")

	var q string
	if args[0] != nil {
		nsp := args[0].(string)
		q = fmt.Sprintf("select * from %s.%s(%s)", nsp, proc, argIDStr)
	} else {
		// use search_path
		q = fmt.Sprintf("select * from %s(%s)", proc, argIDStr)
	}

	return q, argValPrep
}

// -----------------------------------------------------------------------------

// PrepareFuncSQLmap prepares sql query with named args placeholders
func PrepareFuncSQLmap(cfg *AplFlags, args CallDef) (string, []interface{}) {

	var proc string
	ref := args.Proc
	if ref != nil {
		proc = *ref
	} else {
		// fatal - incorrect map structure
	}

	argValPrep := make([]interface{}, len(args.Args))
	argIDs := make([]string, len(args.Args))

	i := 0
	for k, v := range args.Args {
		argIDs[i] = fmt.Sprintf("%s %s $%d", k, cfg.ArgSyntax, i+1)
		argValPrep[i] = v
		i++
	}

	argIDStr := strings.Join(argIDs, ",")

	var q string
	ref = args.Name
	if ref != nil {
		nsp := *ref
		q = fmt.Sprintf("select * from %s.%s(%s)", nsp, proc, argIDStr)
	} else {
		// use search_path
		q = fmt.Sprintf("select * from %s(%s)", proc, argIDStr)
	}

	return q, argValPrep
}

// -----------------------------------------------------------------------------

// FetchSQLResult fetches sql result and marshalls it into json
func FetchSQLResult(rows *pgx.Rows, log *logger.Log) (data *TableRows, err error) {
	// http://stackoverflow.com/a/29164115
	columnDefs := rows.FieldDescriptions()
	//log.Debugf("=========== %+v", columnDefs)
	columns := []string{}
	types := []string{}
	for _, c := range columnDefs {
		columns = append(columns, c.Name)
		types = append(types, c.DataTypeName)
	}

	var tableData TableRows
	for rows.Next() {
		var values []interface{}
		values, err = rows.Values()
		if err != nil {
			log.Warnf("Value fetch error: %s", err.Error())
			return
		}
		log.Debugf("Values: %+v", values)

		entry := make(map[string]interface{})
		for i, col := range columns {
			var v interface{}
			val := values[i]
			if types[i] == "json" || types[i] == "jsonb" {
				if val != nil {
					raw := fmt.Sprintf("%s", val)
					ref := json.RawMessage(raw)
					entry[col] = &ref
				}
			} else {
				v = val
				entry[col] = v
			}
		}
		tableData = append(tableData, entry)

	}
	// Any errors encountered by rows.Next or rows.Scan will be returned here
	if rows.Err() != nil {
		err = rows.Err()
		return
	}
	if tableData == nil {
		log.Warn("Empty result")
		tableData = TableRows{} // empty result is empty array
	}
	data = &tableData
	return
}
<commit_msg>Added support for IsRO func attrib<commit_after>package main

import (
	"encoding/json"
	"fmt"
	"strings"

	"gopkg.in/jackc/pgx.v2"

	"github.com/golang/groupcache"

	"github.com/LeKovr/dbrpc/workman"
	"github.com/LeKovr/go-base/logger"
)

// TableRow holds query result row
type TableRow map[string]interface{}

// TableRows holds slice of query result rows
type TableRows []TableRow

// -----------------------------------------------------------------------------

// Processor gets value from cache and converts it into Result struct
func cacheFetcher(log *logger.Log, cacheGroup *groupcache.Group) workman.WorkerFunc {
	// https://github.com/capotej/groupcache-db-experiment
	return func(payload string) workman.Result {
		var data []byte
		log.Debugf("asked for %s from groupcache", payload)
		err := cacheGroup.Get(nil, payload,
			groupcache.AllocatingByteSliceSink(&data))
		var res workman.Result
		if err != nil {
			res = workman.Result{Success: false, Error: err.Error()}
		} else if len(data) == 0 {
			res = workman.Result{Success: false, Error: "This internal error must be catched earlier. Please contact vendor"}
		} else {
			d := data[1:]
			raw := json.RawMessage(d)
			ok := data[0] == 1
			if ok { // First byte stores success state (1: true, 0: false)
				res = workman.Result{Success: ok, Result: &raw}
			} else {
				res = workman.Result{Success: ok, Error: &raw}
			}
		}
		return res
	}
}

// -----------------------------------------------------------------------------

func dbFetcher(cfg *AplFlags, log *logger.Log, db *pgx.ConnPool) groupcache.GetterFunc {
	return func(ctx groupcache.Context, key string, dest groupcache.Sink) error {
		log.Printf("asking for %s from dbserver", key)

		var isOk byte = 1 // status: success

		data, err := dbQuery(cfg, log, db, key)
		if err != nil {
			log.Warnf("Query for key %s error: %+v", key, err)
			isOk = 0
			data, _ = json.Marshal(err.Error())
		}

		result := []byte{isOk}
		result = append(result, data...)

		dd := result[1:]
		log.Debugf("Save data: %s", dd)
		dest.SetBytes([]byte(result))
		return nil
	}
}

// -----------------------------------------------------------------------------

// FuncDef holds function definition
type FuncDef struct {
	NspName string  // function namespace
	ProName string  // function name
	Permit  *string // permit code
	MaxAge  int     // cache max age
	IsRO    bool    // function is read-only (not volatile)
}

// FuncMap holds map of function definitions
type FuncMap map[string]FuncDef

// -----------------------------------------------------------------------------

func indexFetcher(cfg *AplFlags, log *logger.Log, db *pgx.ConnPool) (index *FuncMap, err error) {
	var rows *pgx.Rows

	q := fmt.Sprintf("select code, nspname, proname, permit_code, max_age, is_ro from %s()", cfg.ArgIndexFunc)
	log.Debugf("Query: %s", q)

	rows, err = db.Query(q)
	if err != nil {
		return
	}

	ind := FuncMap{}
	for rows.Next() {
		var fmr FuncDef
		var code string
		err = rows.Scan(&code, &fmr.NspName, &fmr.ProName, &fmr.Permit, &fmr.MaxAge, &fmr.IsRO)
		if err != nil {
			log.Warnf("Value fetch error: %s", err.Error())
			return
		}
		ind[code] = fmr
	}
	// Any errors encountered by rows.Next or rows.Scan will be returned here
	if rows.Err() != nil {
		err = rows.Err()
		return
	}
	log.Debugf("Index: %+v", ind)
	index = &ind
	return
}

// -----------------------------------------------------------------------------

func dbQuery(cfg *AplFlags, log *logger.Log, db *pgx.ConnPool, key string) (data []byte, err error) {
	var rows *pgx.Rows
	var q string
	var vals []interface{}

	if strings.HasPrefix(key, "[") {
		var args []interface{}
		err = json.Unmarshal([]byte(key), &args)
		if err != nil {
			return
		}
		q, vals = PrepareFuncSQL(cfg, args)
	} else {
		var args CallDef
		err = json.Unmarshal([]byte(key), &args)
		if err != nil {
			return
		}
		q, vals = PrepareFuncSQLmap(cfg, args)
	}
	log.Debugf("Query: %s args: %+v", q, vals)
	rows, err = db.Query(q, vals...)
	if err != nil {
		return
	}

	table, err := FetchSQLResult(rows, log)
	defer rows.Close()
	if err != nil {
		return
	}

	data, err = json.Marshal(*table)
	if err != nil {
		return
	}

	return
}

// -----------------------------------------------------------------------------

// PrepareFuncSQL prepares sql query with args placeholders
func PrepareFuncSQL(cfg *AplFlags, args []interface{}) (string, []interface{}) {
	proc := args[1].(string)
	argVals := args[2:]

	argValPrep := make([]interface{}, len(argVals))
	argIDs := make([]string, len(argVals))

	for i, v := range argVals {
		argIDs[i] = fmt.Sprintf("$%d", i+1)
		argValPrep[i] = v
	}

	argIDStr := strings.Join(argIDs, ",")

	var q string
	if args[0] != nil {
		nsp := args[0].(string)
		q = fmt.Sprintf("select * from %s.%s(%s)", nsp, proc, argIDStr)
	} else {
		// use search_path
		q = fmt.Sprintf("select * from %s(%s)", proc, argIDStr)
	}

	return q, argValPrep
}

// -----------------------------------------------------------------------------

// PrepareFuncSQLmap prepares sql query with named args placeholders
func PrepareFuncSQLmap(cfg *AplFlags, args CallDef) (string, []interface{}) {

	var proc string
	ref := args.Proc
	if ref != nil {
		proc = *ref
	} else {
		// fatal - incorrect map structure
	}

	argValPrep := make([]interface{}, len(args.Args))
	argIDs := make([]string, len(args.Args))

	i := 0
	for k, v := range args.Args {
		argIDs[i] = fmt.Sprintf("%s %s $%d", k, cfg.ArgSyntax, i+1)
		argValPrep[i] = v
		i++
	}

	argIDStr := strings.Join(argIDs, ",")

	var q string
	ref = args.Name
	if ref != nil {
		nsp := *ref
		q = fmt.Sprintf("select * from %s.%s(%s)", nsp, proc, argIDStr)
	} else {
		// use search_path
		q = fmt.Sprintf("select * from %s(%s)", proc, argIDStr)
	}

	return q, argValPrep
}

// -----------------------------------------------------------------------------

// FetchSQLResult fetches sql result and marshalls it into json
func FetchSQLResult(rows *pgx.Rows, log *logger.Log) (data *TableRows, err error) {
	// http://stackoverflow.com/a/29164115
	columnDefs := rows.FieldDescriptions()
	//log.Debugf("=========== %+v", columnDefs)
	columns := []string{}
	types := []string{}
	for _, c := range columnDefs {
		columns = append(columns, c.Name)
		types = append(types, c.DataTypeName)
	}

	var tableData TableRows
	for rows.Next() {
		var values []interface{}
		values, err = rows.Values()
		if err != nil {
			log.Warnf("Value fetch error: %s", err.Error())
			return
		}
		log.Debugf("Values: %+v", values)

		entry := make(map[string]interface{})
		for i, col := range columns {
			var v interface{}
			val := values[i]
			if types[i] == "json" || types[i] == "jsonb" {
				if val != nil {
					raw := fmt.Sprintf("%s", val)
					ref := json.RawMessage(raw)
					entry[col] = &ref
				}
			} else {
				v = val
				entry[col] = v
			}
		}
		tableData = append(tableData, entry)

	}
	// Any errors encountered by rows.Next or rows.Scan will be returned here
	if rows.Err() != nil {
		err = rows.Err()
		return
	}
	if tableData == nil {
		log.Warn("Empty result")
		tableData = TableRows{} // empty result is empty array
	}
	data = &tableData
	return
}
<|endoftext|>


<commit_before>// Copyright 2015 The Cockroach Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
// implied. See the License for the specific language governing
// permissions and limitations under the License.
//
// Author: Kenji Kaneda (kenji.kaneda@gmail.com)

package server

import (
	"os"
	"reflect"
	"testing"
	"time"

	"github.com/cockroachdb/cockroach/gossip/resolver"
	"github.com/cockroachdb/cockroach/util/envutil"
	"github.com/cockroachdb/cockroach/util/leaktest"
	"github.com/cockroachdb/cockroach/util/stop"
)

func TestParseInitNodeAttributes(t *testing.T) {
	defer leaktest.AfterTest(t)()
	ctx := MakeContext()
	ctx.Attrs = "attr1=val1::attr2=val2"
	ctx.Stores = StoreSpecList{Specs: []StoreSpec{{InMemory: true, SizeInBytes: minimumStoreSize * 100}}}
	stopper := stop.NewStopper()
	defer stopper.Stop()
	if err := ctx.InitStores(stopper); err != nil {
		t.Fatalf("Failed to initialize stores: %s", err)
	}
	if err := ctx.InitNode(); err != nil {
		t.Fatalf("Failed to initialize node: %s", err)
	}

	if a, e := ctx.NodeAttributes.Attrs, []string{"attr1=val1", "attr2=val2"}; !reflect.DeepEqual(a, e) {
		t.Fatalf("expected attributes: %v, found: %v", e, a)
	}
}

// TestParseJoinUsingAddrs verifies that JoinList is parsed
// correctly.
func TestParseJoinUsingAddrs(t *testing.T) {
	defer leaktest.AfterTest(t)()
	ctx := MakeContext()
	ctx.JoinList = []string{"localhost:12345,,localhost:23456", "localhost:34567"}
	ctx.Stores = StoreSpecList{Specs: []StoreSpec{{InMemory: true, SizeInBytes: minimumStoreSize * 100}}}
	stopper := stop.NewStopper()
	defer stopper.Stop()
	if err := ctx.InitStores(stopper); err != nil {
		t.Fatalf("Failed to initialize stores: %s", err)
	}
	if err := ctx.InitNode(); err != nil {
		t.Fatalf("Failed to initialize node: %s", err)
	}
	r1, err := resolver.NewResolver(ctx.Context, "localhost:12345")
	if err != nil {
		t.Fatal(err)
	}
	r2, err := resolver.NewResolver(ctx.Context, "localhost:23456")
	if err != nil {
		t.Fatal(err)
	}
	r3, err := resolver.NewResolver(ctx.Context, "localhost:34567")
	if err != nil {
		t.Fatal(err)
	}
	expected := []resolver.Resolver{r1, r2, r3}
	if !reflect.DeepEqual(ctx.GossipBootstrapResolvers, expected) {
		t.Fatalf("Unexpected bootstrap addresses: %v, expected: %v", ctx.GossipBootstrapResolvers, expected)
	}
}

// TestReadEnvironmentVariables verifies that all environment variables are
// correctly parsed.
func TestReadEnvironmentVariables(t *testing.T) {
	defer leaktest.AfterTest(t)()

	resetEnvVar := func() {
		// Reset all environment variables in case any were already set.
		if err := os.Unsetenv("COCKROACH_LINEARIZABLE"); err != nil {
			t.Fatal(err)
		}
		if err := os.Unsetenv("COCKROACH_MAX_OFFSET"); err != nil {
			t.Fatal(err)
		}
		if err := os.Unsetenv("COCKROACH_METRICS_SAMPLE_INTERVAL"); err != nil {
			t.Fatal(err)
		}
		if err := os.Unsetenv("COCKROACH_SCAN_INTERVAL"); err != nil {
			t.Fatal(err)
		}
		if err := os.Unsetenv("COCKROACH_SCAN_MAX_IDLE_TIME"); err != nil {
			t.Fatal(err)
		}
		if err := os.Unsetenv("COCKROACH_CONSISTENCY_CHECK_INTERVAL"); err != nil {
			t.Fatal(err)
		}
		if err := os.Unsetenv("COCKROACH_CONSISTENCY_CHECK_PANIC_ON_FAILURE"); err != nil {
			t.Fatal(err)
		}
		if err := os.Unsetenv("COCKROACH_TIME_UNTIL_STORE_DEAD"); err != nil {
			t.Fatal(err)
		}
		if err := os.Unsetenv("COCKROACH_CONSISTENCY_CHECK_INTERVAL"); err != nil {
			t.Fatal(err)
		}
		if err := os.Unsetenv("COCKROACH_RESERVATIONS_ENABLED"); err != nil {
			t.Fatal(err)
		}
	}
	defer resetEnvVar()

	// Makes sure no values are set when no environment variables are set.
	ctx := MakeContext()
	ctxExpected := MakeContext()

	resetEnvVar()
	ctx.readEnvironmentVariables()
	if !reflect.DeepEqual(ctx, ctxExpected) {
		t.Fatalf("actual context does not match expected:\nactual:%+v\nexpected:%+v", ctx, ctxExpected)
	}

	// Set all the environment variables to valid values and ensure they are set
	// correctly.
	if err := os.Setenv("COCKROACH_LINEARIZABLE", "true"); err != nil {
		t.Fatal(err)
	}
	ctxExpected.Linearizable = true
	if err := os.Setenv("COCKROACH_MAX_OFFSET", "1s"); err != nil {
		t.Fatal(err)
	}
	ctxExpected.MaxOffset = time.Second
	if err := os.Setenv("COCKROACH_METRICS_SAMPLE_INTERVAL", "1h10m"); err != nil {
		t.Fatal(err)
	}
	ctxExpected.MetricsSampleInterval = time.Hour + time.Minute*10
	if err := os.Setenv("COCKROACH_SCAN_INTERVAL", "48h"); err != nil {
		t.Fatal(err)
	}
	ctxExpected.ScanInterval = time.Hour * 48
	if err := os.Setenv("COCKROACH_SCAN_MAX_IDLE_TIME", "100ns"); err != nil {
		t.Fatal(err)
	}
	ctxExpected.ScanMaxIdleTime = time.Nanosecond * 100
	if err := os.Setenv("COCKROACH_CONSISTENCY_CHECK_INTERVAL", "48h"); err != nil {
		t.Fatal(err)
	}
	ctxExpected.ConsistencyCheckInterval = time.Hour * 48
	if err := os.Setenv("COCKROACH_CONSISTENCY_CHECK_PANIC_ON_FAILURE", "true"); err != nil {
		t.Fatal(err)
	}
	ctxExpected.ConsistencyCheckPanicOnFailure = true
	if err := os.Setenv("COCKROACH_TIME_UNTIL_STORE_DEAD", "10ms"); err != nil {
		t.Fatal(err)
	}
	ctxExpected.TimeUntilStoreDead = time.Millisecond * 10
	if err := os.Setenv("COCKROACH_CONSISTENCY_CHECK_INTERVAL", "10ms"); err != nil {
		t.Fatal(err)
	}
	ctxExpected.ConsistencyCheckInterval = time.Millisecond * 10
	if err := os.Setenv("COCKROACH_RESERVATIONS_ENABLED", "false"); err != nil {
		t.Fatal(err)
	}
	ctxExpected.ReservationsEnabled = false

	envutil.ClearEnvCache()
	ctx.readEnvironmentVariables()
	if !reflect.DeepEqual(ctx, ctxExpected) {
		t.Fatalf("actual context does not match expected:\nactual:%+v\nexpected:%+v", ctx, ctxExpected)
	}

	// Set all the environment variables to invalid values and test that the
	// defaults are still set.
	ctx = MakeContext()
	ctxExpected = MakeContext()

	if err := os.Setenv("COCKROACH_LINEARIZABLE", "abcd"); err != nil {
		t.Fatal(err)
	}
	if err := os.Setenv("COCKROACH_MAX_OFFSET", "abcd"); err != nil {
		t.Fatal(err)
	}
	if err := os.Setenv("COCKROACH_METRICS_SAMPLE_INTERVAL", "abcd"); err != nil {
		t.Fatal(err)
	}
	if err := os.Setenv("COCKROACH_SCAN_INTERVAL", "abcd"); err != nil {
		t.Fatal(err)
	}
	if err := os.Setenv("COCKROACH_SCAN_MAX_IDLE_TIME", "abcd"); err != nil {
		t.Fatal(err)
	}
	if err := os.Setenv("COCKROACH_CONSISTENCY_CHECK_INTERVAL", "abcd"); err != nil {
		t.Fatal(err)
	}
	if err := os.Setenv("COCKROACH_CONSISTENCY_CHECK_PANIC_ON_FAILURE", "abcd"); err != nil {
		t.Fatal(err)
	}
	if err := os.Setenv("COCKROACH_TIME_UNTIL_STORE_DEAD", "abcd"); err != nil {
		t.Fatal(err)
	}
	if err := os.Setenv("COCKROACH_CONSISTENCY_CHECK_INTERVAL", "abcd"); err != nil {
		t.Fatal(err)
	}
	if err := os.Setenv("COCKROACH_RESERVATIONS_ENABLED", "abcd"); err != nil {
		t.Fatal(err)
	}

	envutil.ClearEnvCache()
	ctx.readEnvironmentVariables()
	if !reflect.DeepEqual(ctx, ctxExpected) {
		t.Fatalf("actual context does not match expected:\nactual:%+v\nexpected:%+v", ctx, ctxExpected)
	}
}
<commit_msg>server: Clear the env cache after resetting environment variables<commit_after>// Copyright 2015 The Cockroach Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
// implied. See the License for the specific language governing
// permissions and limitations under the License.
//
// Author: Kenji Kaneda (kenji.kaneda@gmail.com)

package server

import (
	"os"
	"reflect"
	"testing"
	"time"

	"github.com/cockroachdb/cockroach/gossip/resolver"
	"github.com/cockroachdb/cockroach/util/envutil"
	"github.com/cockroachdb/cockroach/util/leaktest"
	"github.com/cockroachdb/cockroach/util/stop"
)

func TestParseInitNodeAttributes(t *testing.T) {
	defer leaktest.AfterTest(t)()
	ctx := MakeContext()
	ctx.Attrs = "attr1=val1::attr2=val2"
	ctx.Stores = StoreSpecList{Specs: []StoreSpec{{InMemory: true, SizeInBytes: minimumStoreSize * 100}}}
	stopper := stop.NewStopper()
	defer stopper.Stop()
	if err := ctx.InitStores(stopper); err != nil {
		t.Fatalf("Failed to initialize stores: %s", err)
	}
	if err := ctx.InitNode(); err != nil {
		t.Fatalf("Failed to initialize node: %s", err)
	}

	if a, e := ctx.NodeAttributes.Attrs, []string{"attr1=val1", "attr2=val2"}; !reflect.DeepEqual(a, e) {
		t.Fatalf("expected attributes: %v, found: %v", e, a)
	}
}

// TestParseJoinUsingAddrs verifies that JoinList is parsed
// correctly.
func TestParseJoinUsingAddrs(t *testing.T) {
	defer leaktest.AfterTest(t)()
	ctx := MakeContext()
	ctx.JoinList = []string{"localhost:12345,,localhost:23456", "localhost:34567"}
	ctx.Stores = StoreSpecList{Specs: []StoreSpec{{InMemory: true, SizeInBytes: minimumStoreSize * 100}}}
	stopper := stop.NewStopper()
	defer stopper.Stop()
	if err := ctx.InitStores(stopper); err != nil {
		t.Fatalf("Failed to initialize stores: %s", err)
	}
	if err := ctx.InitNode(); err != nil {
		t.Fatalf("Failed to initialize node: %s", err)
	}
	r1, err := resolver.NewResolver(ctx.Context, "localhost:12345")
	if err != nil {
		t.Fatal(err)
	}
	r2, err := resolver.NewResolver(ctx.Context, "localhost:23456")
	if err != nil {
		t.Fatal(err)
	}
	r3, err := resolver.NewResolver(ctx.Context, "localhost:34567")
	if err != nil {
		t.Fatal(err)
	}
	expected := []resolver.Resolver{r1, r2, r3}
	if !reflect.DeepEqual(ctx.GossipBootstrapResolvers, expected) {
		t.Fatalf("Unexpected bootstrap addresses: %v, expected: %v", ctx.GossipBootstrapResolvers, expected)
	}
}

// TestReadEnvironmentVariables verifies that all environment variables are
// correctly parsed.
func TestReadEnvironmentVariables(t *testing.T) {
	defer leaktest.AfterTest(t)()

	resetEnvVar := func() {
		// Reset all environment variables in case any were already set.
		if err := os.Unsetenv("COCKROACH_LINEARIZABLE"); err != nil {
			t.Fatal(err)
		}
		if err := os.Unsetenv("COCKROACH_MAX_OFFSET"); err != nil {
			t.Fatal(err)
		}
		if err := os.Unsetenv("COCKROACH_METRICS_SAMPLE_INTERVAL"); err != nil {
			t.Fatal(err)
		}
		if err := os.Unsetenv("COCKROACH_SCAN_INTERVAL"); err != nil {
			t.Fatal(err)
		}
		if err := os.Unsetenv("COCKROACH_SCAN_MAX_IDLE_TIME"); err != nil {
			t.Fatal(err)
		}
		if err := os.Unsetenv("COCKROACH_CONSISTENCY_CHECK_INTERVAL"); err != nil {
			t.Fatal(err)
		}
		if err := os.Unsetenv("COCKROACH_CONSISTENCY_CHECK_PANIC_ON_FAILURE"); err != nil {
			t.Fatal(err)
		}
		if err := os.Unsetenv("COCKROACH_TIME_UNTIL_STORE_DEAD"); err != nil {
			t.Fatal(err)
		}
		if err := os.Unsetenv("COCKROACH_CONSISTENCY_CHECK_INTERVAL"); err != nil {
			t.Fatal(err)
		}
		if err := os.Unsetenv("COCKROACH_RESERVATIONS_ENABLED"); err != nil {
			t.Fatal(err)
		}
		envutil.ClearEnvCache()
	}
	defer resetEnvVar()

	// Makes sure no values are set when no environment variables are set.
	ctx := MakeContext()
	ctxExpected := MakeContext()

	resetEnvVar()
	ctx.readEnvironmentVariables()
	if !reflect.DeepEqual(ctx, ctxExpected) {
		t.Fatalf("actual context does not match expected:\nactual:%+v\nexpected:%+v", ctx, ctxExpected)
	}

	// Set all the environment variables to valid values and ensure they are set
	// correctly.
	if err := os.Setenv("COCKROACH_LINEARIZABLE", "true"); err != nil {
		t.Fatal(err)
	}
	ctxExpected.Linearizable = true
	if err := os.Setenv("COCKROACH_MAX_OFFSET", "1s"); err != nil {
		t.Fatal(err)
	}
	ctxExpected.MaxOffset = time.Second
	if err := os.Setenv("COCKROACH_METRICS_SAMPLE_INTERVAL", "1h10m"); err != nil {
		t.Fatal(err)
	}
	ctxExpected.MetricsSampleInterval = time.Hour + time.Minute*10
	if err := os.Setenv("COCKROACH_SCAN_INTERVAL", "48h"); err != nil {
		t.Fatal(err)
	}
	ctxExpected.ScanInterval = time.Hour * 48
	if err := os.Setenv("COCKROACH_SCAN_MAX_IDLE_TIME", "100ns"); err != nil {
		t.Fatal(err)
	}
	ctxExpected.ScanMaxIdleTime = time.Nanosecond * 100
	if err := os.Setenv("COCKROACH_CONSISTENCY_CHECK_INTERVAL", "48h"); err != nil {
		t.Fatal(err)
	}
	ctxExpected.ConsistencyCheckInterval = time.Hour * 48
	if err := os.Setenv("COCKROACH_CONSISTENCY_CHECK_PANIC_ON_FAILURE", "true"); err != nil {
		t.Fatal(err)
	}
	ctxExpected.ConsistencyCheckPanicOnFailure = true
	if err := os.Setenv("COCKROACH_TIME_UNTIL_STORE_DEAD", "10ms"); err != nil {
		t.Fatal(err)
	}
	ctxExpected.TimeUntilStoreDead = time.Millisecond * 10
	if err := os.Setenv("COCKROACH_CONSISTENCY_CHECK_INTERVAL", "10ms"); err != nil {
		t.Fatal(err)
	}
	ctxExpected.ConsistencyCheckInterval = time.Millisecond * 10
	if err := os.Setenv("COCKROACH_RESERVATIONS_ENABLED", "false"); err != nil {
		t.Fatal(err)
	}
	ctxExpected.ReservationsEnabled = false

	envutil.ClearEnvCache()
	ctx.readEnvironmentVariables()
	if !reflect.DeepEqual(ctx, ctxExpected) {
		t.Fatalf("actual context does not match expected:\nactual:%+v\nexpected:%+v", ctx, ctxExpected)
	}

	// Set all the environment variables to invalid values and test that the
	// defaults are still set.
	ctx = MakeContext()
	ctxExpected = MakeContext()

	if err := os.Setenv("COCKROACH_LINEARIZABLE", "abcd"); err != nil {
		t.Fatal(err)
	}
	if err := os.Setenv("COCKROACH_MAX_OFFSET", "abcd"); err != nil {
		t.Fatal(err)
	}
	if err := os.Setenv("COCKROACH_METRICS_SAMPLE_INTERVAL", "abcd"); err != nil {
		t.Fatal(err)
	}
	if err := os.Setenv("COCKROACH_SCAN_INTERVAL", "abcd"); err != nil {
		t.Fatal(err)
	}
	if err := os.Setenv("COCKROACH_SCAN_MAX_IDLE_TIME", "abcd"); err != nil {
		t.Fatal(err)
	}
	if err := os.Setenv("COCKROACH_CONSISTENCY_CHECK_INTERVAL", "abcd"); err != nil {
		t.Fatal(err)
	}
	if err := os.Setenv("COCKROACH_CONSISTENCY_CHECK_PANIC_ON_FAILURE", "abcd"); err != nil {
		t.Fatal(err)
	}
	if err := os.Setenv("COCKROACH_TIME_UNTIL_STORE_DEAD", "abcd"); err != nil {
		t.Fatal(err)
	}
	if err := os.Setenv("COCKROACH_CONSISTENCY_CHECK_INTERVAL", "abcd"); err != nil {
		t.Fatal(err)
	}
	if err := os.Setenv("COCKROACH_RESERVATIONS_ENABLED", "abcd"); err != nil {
		t.Fatal(err)
	}

	envutil.ClearEnvCache()
	ctx.readEnvironmentVariables()
	if !reflect.DeepEqual(ctx, ctxExpected) {
		t.Fatalf("actual context does not match expected:\nactual:%+v\nexpected:%+v", ctx, ctxExpected)
	}
}
<|endoftext|>


<commit_before>package api

import (
	"encoding/json"
	"fmt"
	"github.com/gorilla/context"
	"github.com/gorilla/mux"
	"github.com/martinp/atbapi/atb"
	"github.com/pmylund/go-cache"
	"log"
	"net/http"
	"strconv"
	"time"
)

type Api struct {
	Client atb.Client
	cache  *cache.Cache
	expiration
}

type expiration struct {
	departures time.Duration
	stops      time.Duration
}

func marshal(data interface{}, indent bool) ([]byte, error) {
	if indent {
		return json.MarshalIndent(data, "", "  ")
	}
	return json.Marshal(data)
}

func (a *Api) getBusStops() (BusStops, error) {
	const cacheKey = "stops"
	cached, ok := a.cache.Get(cacheKey)
	if ok {
		cachedBusStops, ok := cached.(BusStops)
		if !ok {
			return BusStops{}, fmt.Errorf(
				"type assertion of cached value failed")
		}
		return cachedBusStops, nil
	}
	atbBusStops, err := a.Client.GetBusStops()
	if err != nil {
		return BusStops{}, err
	}
	busStops, err := convertBusStops(atbBusStops)
	if err != nil {
		return BusStops{}, err
	}
	// Create a map of nodeIds
	busStops.nodeIds = make(map[int]struct{}, len(busStops.Stops))
	for _, s := range busStops.Stops {
		busStops.nodeIds[s.NodeId] = struct{}{}
	}
	a.cache.Set(cacheKey, busStops, a.expiration.stops)
	return busStops, nil
}

func (a *Api) getDepartures(nodeId int) (Departures, error) {
	cacheKey := strconv.Itoa(nodeId)
	cached, ok := a.cache.Get(cacheKey)
	if ok {
		cachedDepartures, ok := cached.(Departures)
		if !ok {
			return Departures{}, fmt.Errorf(
				"type assertion of cached value failed")
		}
		return cachedDepartures, nil
	}
	forecasts, err := a.Client.GetRealTimeForecast(nodeId)
	if err != nil {
		return Departures{}, err
	}
	departures, err := convertForecasts(forecasts)
	if err != nil {
		return Departures{}, err
	}
	a.cache.Set(cacheKey, departures, cache.DefaultExpiration)
	return departures, nil
}

func (a *Api) BusStopsHandler(w http.ResponseWriter, req *http.Request) *Error {
	busStops, err := a.getBusStops()
	if err != nil {
		return &Error{
			err:     err,
			Status:  http.StatusInternalServerError,
			Message: "failed to get bus stops from atb",
		}
	}
	jsonBlob, err := marshal(busStops, context.Get(req, "indent").(bool))
	if err != nil {
		return &Error{
			err:     err,
			Status:  http.StatusInternalServerError,
			Message: "failed to marshal bus stops",
		}
	}
	w.Write(jsonBlob)
	return nil
}

func (a *Api) DeparturesHandler(w http.ResponseWriter, req *http.Request) *Error {
	vars := mux.Vars(req)
	nodeId, err := strconv.Atoi(vars["nodeId"])
	if err != nil {
		return &Error{
			err:     err,
			Status:  http.StatusBadRequest,
			Message: "missing or invalid nodeId",
		}
	}
	busStops, err := a.getBusStops()
	if err != nil {
		return &Error{
			err:     err,
			Status:  http.StatusInternalServerError,
			Message: "could not get bus stops from atb",
		}
	}
	_, knownBusStop := busStops.nodeIds[nodeId]
	if !knownBusStop {
		msg := fmt.Sprintf("bus stop with nodeId=%d not found", nodeId)
		return &Error{
			err:     err,
			Status:  http.StatusNotFound,
			Message: msg,
		}
	}
	departures, err := a.getDepartures(nodeId)
	if err != nil {
		return &Error{
			err:     err,
			Status:  http.StatusInternalServerError,
			Message: "could not get departures from atb",
		}
	}
	jsonBlob, err := marshal(departures, context.Get(req, "indent").(bool))
	if err != nil {
		return &Error{
			err:     err,
			Status:  http.StatusInternalServerError,
			Message: "failed to marshal departures",
		}
	}
	w.Write(jsonBlob)
	return nil
}

func (a *Api) NotFoundHandler(w http.ResponseWriter, req *http.Request) *Error {
	return &Error{
		err:     nil,
		Status:  http.StatusNotFound,
		Message: "route not found",
	}
}

func New(client atb.Client, stopsExpiration, depExpiration time.Duration) Api {
	cache := cache.New(depExpiration, 30*time.Second)
	return Api{
		Client: client,
		cache:  cache,
		expiration: expiration{
			stops:      stopsExpiration,
			departures: depExpiration,
		},
	}
}

type appHandler func(http.ResponseWriter, *http.Request) *Error

func (fn appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	if e := fn(w, r); e != nil { // e is *Error, not os.Error.
		if e.err != nil {
			log.Print(e.err)
		}
		data, err := marshal(e, true)
		if err != nil {
			// Should never happen
			panic(err)
		}
		w.WriteHeader(e.Status)
		w.Write(data)
	}
}

func requestFilter(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		_, indent := r.URL.Query()["pretty"]
		context.Set(r, "indent", indent)
		w.Header().Set("Content-Type", "application/json")
		next.ServeHTTP(w, r)
	})
}

func (a *Api) ListenAndServe(addr string) error {
	r := mux.NewRouter()
	r.Handle("/api/v1/busstops", appHandler(a.BusStopsHandler))
	r.Handle("/api/v1/departures/{nodeId:[0-9]+}",
		appHandler(a.DeparturesHandler))
	r.NotFoundHandler = appHandler(a.NotFoundHandler)
	http.Handle("/", requestFilter(r))
	return http.ListenAndServe(addr, nil)
}
<commit_msg>Handle JSON marshalling in ServeHTTP<commit_after>package api

import (
	"encoding/json"
	"fmt"
	"github.com/gorilla/context"
	"github.com/gorilla/mux"
	"github.com/martinp/atbapi/atb"
	"github.com/pmylund/go-cache"
	"log"
	"net/http"
	"strconv"
	"time"
)

type Api struct {
	Client atb.Client
	cache  *cache.Cache
	expiration
}

type expiration struct {
	departures time.Duration
	stops      time.Duration
}

func marshal(data interface{}, indent bool) ([]byte, error) {
	if indent {
		return json.MarshalIndent(data, "", "  ")
	}
	return json.Marshal(data)
}

func (a *Api) getBusStops() (BusStops, error) {
	const cacheKey = "stops"
	cached, ok := a.cache.Get(cacheKey)
	if ok {
		cachedBusStops, ok := cached.(BusStops)
		if !ok {
			return BusStops{}, fmt.Errorf(
				"type assertion of cached value failed")
		}
		return cachedBusStops, nil
	}
	atbBusStops, err := a.Client.GetBusStops()
	if err != nil {
		return BusStops{}, err
	}
	busStops, err := convertBusStops(atbBusStops)
	if err != nil {
		return BusStops{}, err
	}
	// Create a map of nodeIds
	busStops.nodeIds = make(map[int]struct{}, len(busStops.Stops))
	for _, s := range busStops.Stops {
		busStops.nodeIds[s.NodeId] = struct{}{}
	}
	a.cache.Set(cacheKey, busStops, a.expiration.stops)
	return busStops, nil
}

func (a *Api) getDepartures(nodeId int) (Departures, error) {
	cacheKey := strconv.Itoa(nodeId)
	cached, ok := a.cache.Get(cacheKey)
	if ok {
		cachedDepartures, ok := cached.(Departures)
		if !ok {
			return Departures{}, fmt.Errorf(
				"type assertion of cached value failed")
		}
		return cachedDepartures, nil
	}
	forecasts, err := a.Client.GetRealTimeForecast(nodeId)
	if err != nil {
		return Departures{}, err
	}
	departures, err := convertForecasts(forecasts)
	if err != nil {
		return Departures{}, err
	}
	a.cache.Set(cacheKey, departures, cache.DefaultExpiration)
	return departures, nil
}

func (a *Api) BusStopsHandler(w http.ResponseWriter, req *http.Request) (interface{}, *Error) {
	busStops, err := a.getBusStops()
	if err != nil {
		return nil, &Error{
			err:     err,
			Status:  http.StatusInternalServerError,
			Message: "failed to get bus stops from atb",
		}
	}
	return busStops, nil
}

func (a *Api) DeparturesHandler(w http.ResponseWriter, req *http.Request) (interface{}, *Error) {
	vars := mux.Vars(req)
	nodeId, err := strconv.Atoi(vars["nodeId"])
	if err != nil {
		return nil, &Error{
			err:     err,
			Status:  http.StatusBadRequest,
			Message: "missing or invalid nodeId",
		}
	}
	busStops, err := a.getBusStops()
	if err != nil {
		return nil, &Error{
			err:     err,
			Status:  http.StatusInternalServerError,
			Message: "could not get bus stops from atb",
		}
	}
	_, knownBusStop := busStops.nodeIds[nodeId]
	if !knownBusStop {
		msg := fmt.Sprintf("bus stop with nodeId=%d not found", nodeId)
		return nil, &Error{
			err:     err,
			Status:  http.StatusNotFound,
			Message: msg,
		}
	}
	departures, err := a.getDepartures(nodeId)
	if err != nil {
		return nil, &Error{
			err:     err,
			Status:  http.StatusInternalServerError,
			Message: "could not get departures from atb",
		}
	}
	return departures, nil
}

func (a *Api) NotFoundHandler(w http.ResponseWriter, req *http.Request) (interface{}, *Error) {
	return nil, &Error{
		err:     nil,
		Status:  http.StatusNotFound,
		Message: "route not found",
	}
}

func New(client atb.Client, stopsExpiration, depExpiration time.Duration) Api {
	cache := cache.New(depExpiration, 30*time.Second)
	return Api{
		Client: client,
		cache:  cache,
		expiration: expiration{
			stops:      stopsExpiration,
			departures: depExpiration,
		},
	}
}

type appHandler func(http.ResponseWriter, *http.Request) (interface{}, *Error)

func (fn appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	data, e := fn(w, r)
	if e != nil { // e is *Error, not os.Error.
		if e.err != nil {
			log.Print(e.err)
		}
		jsonBlob, err := marshal(e, true)
		if err != nil {
			// Should never happen
			panic(err)
		}
		w.WriteHeader(e.Status)
		w.Write(jsonBlob)
	} else {
		indent := context.Get(r, "indent").(bool)
		jsonBlob, err := marshal(data, indent)
		if err != nil {
			panic(err)
		}
		w.Write(jsonBlob)
	}
}

func requestFilter(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		_, indent := r.URL.Query()["pretty"]
		context.Set(r, "indent", indent)
		w.Header().Set("Content-Type", "application/json")
		next.ServeHTTP(w, r)
	})
}

func (a *Api) ListenAndServe(addr string) error {
	r := mux.NewRouter()
	r.Handle("/api/v1/busstops", appHandler(a.BusStopsHandler))
	r.Handle("/api/v1/departures/{nodeId:[0-9]+}",
		appHandler(a.DeparturesHandler))
	r.NotFoundHandler = appHandler(a.NotFoundHandler)
	http.Handle("/", requestFilter(r))
	return http.ListenAndServe(addr, nil)
}
<|endoftext|>