|
|
package story
import ( "crypto/rand" "encoding/binary" "errors" "fmt" "os" "strconv" "time"
"git.aiterp.net/rpdata/api/internal/store" "github.com/globalsign/mgo" "github.com/globalsign/mgo/bson" )
var storyCollection *mgo.Collection
// ErrTagAlreadyExists is an error returned by Story.AddTag
var ErrTagAlreadyExists = errors.New("Tag already exists")
// ErrTagNotExists is an error returned by Story.RemoveTag
var ErrTagNotExists = errors.New("Tag does not exist")
// A Story is user content that does not have a wiki-suitable format. Documents, new stories, short stories, and so on.
// The story model is a container for multiple chapters this time, in contrast to the previous version.
type Story struct { ID string `bson:"_id"` Author string `bson:"author"` Name string `bson:"name"` Category string `bson:"category"` Open bool `bson:"open"` Listed bool `bson:"listed"` Tags []Tag `bson:"tags"` CreatedDate time.Time `bson:"createdDate"` FictionalDate time.Time `bson:"fictionalDate,omitempty"` UpdatedDate time.Time `bson:"updatedDate"` }
// AddTag adds a tag to the story. It returns ErrTagAlreadyExists if the tag is already there
func (story *Story) AddTag(tag Tag) error { for i := range story.Tags { if story.Tags[i].Equal(tag) { return ErrTagAlreadyExists } }
err := storyCollection.UpdateId(story.ID, bson.M{"$push": bson.M{"tags": tag}}) if err != nil { return err }
story.Tags = append(story.Tags, tag)
return nil }
// RemoveTag removes a tag to the story. It returns ErrTagNotExists if the tag does not exist.
func (story *Story) RemoveTag(tag Tag) error { index := -1 for i := range story.Tags { if story.Tags[i].Equal(tag) { index = i break } } if index == -1 { return ErrTagNotExists }
err := storyCollection.UpdateId(story.ID, bson.M{"$pull": bson.M{"tags": tag}}) if err != nil { return err }
story.Tags = append(story.Tags[:index], story.Tags[index+1:]...)
return nil }
// Edit edits the story, reflecting the new values in the story's struct values. If nothing will be
// changed, it will silently return without a database roundtrip.
func (story *Story) Edit(name, category *string, listed, open *bool, fictionalDate *time.Time) error { changes := bson.M{} changed := *story
if name != nil && *name != story.Name { changes["name"] = *name changed.Name = *name } if category != nil && *category != story.Category { changes["category"] = *category changed.Name = *category } if listed != nil && *listed != story.Listed { changes["listed"] = *listed changed.Listed = *listed } if open != nil && *open != story.Open { changes["open"] = *open changed.Open = *open } if fictionalDate != nil && !fictionalDate.Equal(story.FictionalDate) { changes["fictionalDate"] = *fictionalDate changed.FictionalDate = *fictionalDate }
if len(changes) == 0 { return nil }
err := storyCollection.UpdateId(story.ID, bson.M{"$set": changes}) if err != nil { return err }
*story = changed
return nil }
// Remove the story from the database
func (story *Story) Remove() error { return storyCollection.RemoveId(story.ID) }
// Chapters calls ListChapterStoryID with the story's ID:
func (story *Story) Chapters() ([]Chapter, error) { return ListChapterStoryID(story.ID) }
// AddChapter adds a chapter to the story. This does not enforce the `Open` setting, but it will log a warning if it
// occurs
func (story *Story) AddChapter(title, author, source string, createdDate, finctionalDate time.Time) (Chapter, error) { if !story.Open && author != story.Author { fmt.Fprintf(os.Stderr, "WARNING: AddChapter is breaking Open rules (story.id=%#+v, story.name=%#+v, chapter.author=%#+v, chapter.title=%#+v)", story.ID, story.Name, author, title) }
chapter := Chapter{ ID: makeChapterID(), StoryID: story.ID, Title: title, Author: author, Source: source, CreatedDate: createdDate, FictionalDate: finctionalDate, EditedDate: createdDate, }
err := chapterCollection.Insert(chapter) if err != nil { return Chapter{}, err }
if createdDate.After(story.UpdatedDate) { if err := storyCollection.UpdateId(story.ID, bson.M{"$set": bson.M{"updatedDate": createdDate}}); err == nil { story.UpdatedDate = createdDate } }
return chapter, nil }
// New creates a new story.
func New(name, author, category string, listed, open bool, tags []Tag, createdDate, fictionalDate time.Time) (Story, error) { story := Story{ ID: makeStoryID(), Name: name, Author: author, Category: category, Listed: listed, Open: open, Tags: tags, CreatedDate: createdDate, FictionalDate: fictionalDate, UpdatedDate: createdDate, }
err := storyCollection.Insert(story) if err != nil { return Story{}, err }
return story, nil }
// FindID finds a story by ID
func FindID(id string) (Story, error) { story := Story{} err := storyCollection.FindId(id).One(&story)
return story, err }
// List lists stories by any non-zero criteria passed with it.
func List(author string, tags []Tag, earliest, latest time.Time, limit int) ([]Story, error) { query := bson.M{}
if author != "" { query["author"] = author }
if len(tags) > 0 { query["tags"] = bson.M{"$in": tags} }
if !earliest.IsZero() && !latest.IsZero() { query["fictionalDate"] = bson.M{ "$gte": earliest, "$lt": latest, } } else if !latest.IsZero() { query["fictionalDate"] = bson.M{ "$lt": latest, } } else if !earliest.IsZero() { query["fictionalDate"] = bson.M{ "$gte": earliest, } }
stories := make([]Story, 0, 128) err := storyCollection.Find(query).Limit(limit).All(&stories)
return stories, err }
// makeStoryID makes a random story ID that's 16 characters long
func makeStoryID() string { result := "S" offset := 0 data := make([]byte, 32)
rand.Read(data) for len(result) < 16 { result += strconv.FormatUint(binary.LittleEndian.Uint64(data[offset:]), 36) offset += 8
if offset >= 32 { rand.Read(data) offset = 0 } }
return result[:16] }
func init() { store.HandleInit(func(db *mgo.Database) { storyCollection = db.C("story.stories")
storyCollection.EnsureIndexKey("tags") storyCollection.EnsureIndexKey("author") storyCollection.EnsureIndexKey("updatedDate") storyCollection.EnsureIndexKey("fictionalDate") storyCollection.EnsureIndexKey("listed") }) }
|