Add initial project files
This commit is contained in:
75
pkg/deesee/codec.go
Normal file
75
pkg/deesee/codec.go
Normal file
@@ -0,0 +1,75 @@
|
||||
// Package deesee contains functions for encrypting and decrypting text.
|
||||
//
|
||||
// Important:
|
||||
// - The encryption is not secure at all, it's just a simple substitution cipher.
|
||||
// - Only the English language is supported.
|
||||
//
|
||||
// Ideas:
|
||||
// - write an encoder and construct like NewEncoder(key).Encode(any), but this is not required at this stage of the project.
|
||||
// - improve deesee-encoder to work with annotations, something like `deesee:"encode"` to prevent encoding for not authorized properties
|
||||
package deesee
|
||||
|
||||
import "strings"
|
||||
|
||||
const (
|
||||
a = 97 // ASCII code for a
|
||||
z = 122 // ASCII code for z
|
||||
)
|
||||
|
||||
// Encrypt text and returns an encrypted using the "DeeSee Chiffre" encryption.
|
||||
func Encrypt(text string, n int) string {
|
||||
return chiffre(text, n)
|
||||
}
|
||||
|
||||
// Decrypt text and returns a decrypted using the "DeeSee Chiffre" encryption.
|
||||
func Decrypt(text string, n int) string {
|
||||
return chiffre(text, -n)
|
||||
}
|
||||
|
||||
// chiffre function takes a string and a key as input and returns the DeeSee encoded string
|
||||
func chiffre(text string, key int) string {
|
||||
var (
|
||||
output strings.Builder // String builder to store the encoded string
|
||||
ascii int // ASCII value of the current character
|
||||
)
|
||||
//
|
||||
// Convert the text string to lowercase
|
||||
text = strings.ToLower(text)
|
||||
|
||||
// Let the key be in the range of letters
|
||||
key %= z - a - 1
|
||||
|
||||
// Iterate through each character in the text string
|
||||
for _, char := range text {
|
||||
// Convert the character to its ASCII value
|
||||
ascii = int(char)
|
||||
|
||||
// Encode only if character is a letter
|
||||
if isLetter(ascii) {
|
||||
// Shift the character by the key
|
||||
ascii += key
|
||||
|
||||
// Which direction are we shifting?
|
||||
if key > 0 {
|
||||
// If the character is now out of bounds
|
||||
if ascii > z {
|
||||
ascii = a + key - 2
|
||||
}
|
||||
} else {
|
||||
// If the character is now out of bounds
|
||||
if ascii < a {
|
||||
ascii = z + key + 2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Append the encoded character to the output string
|
||||
output.WriteRune(rune(ascii))
|
||||
}
|
||||
return output.String()
|
||||
}
|
||||
|
||||
// isLetter returns true if the given character is inside of ASCII letter diapason.
|
||||
func isLetter(char int) bool {
|
||||
return char >= a && char <= z
|
||||
}
|
||||
36
pkg/deesee/codec_test.go
Normal file
36
pkg/deesee/codec_test.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package deesee
|
||||
|
||||
import "testing"
|
||||
|
||||
// TestEncrypt tests the Encrypt and Decrypt function.
|
||||
func TestCryptDecrypt(t *testing.T) {
|
||||
|
||||
// Case is a struct to store test cases.
|
||||
type Case struct {
|
||||
name string
|
||||
key int
|
||||
A string
|
||||
B string
|
||||
}
|
||||
|
||||
// Run the test cases.
|
||||
for _, tt := range []Case{
|
||||
{"Test encrypt-decrypt: simple text",
|
||||
5, "clark", "hqfwp"},
|
||||
{"Test encrypt-decrypt: text with shifted outbounds chars",
|
||||
3, "cherry blossom", "fkhuub eorvvrp"},
|
||||
{"Test encrypt-decrypt: text with shifted outbounds chars and non-letter chars",
|
||||
3, "cherry123 blossom!", "fkhuub123 eorvvrp!"},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
enc := Encrypt(tt.A, tt.key)
|
||||
if enc != tt.B {
|
||||
t.Errorf("Encrypt() = %v, want %v", enc, tt.B)
|
||||
}
|
||||
dec := Decrypt(enc, tt.key)
|
||||
if dec != tt.A {
|
||||
t.Errorf("Decrypt() = %v, want %v", dec, tt.A)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
39
pkg/deesee/filter_test.go
Normal file
39
pkg/deesee/filter_test.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package deesee
|
||||
|
||||
import (
|
||||
"github.com/eslider/superherohub/pkg/file"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Unit Test Coverage
|
||||
func TestFilterBySuperPowers(t *testing.T) {
|
||||
rootPath, err := file.GetModRootPath()
|
||||
if err != nil {
|
||||
t.Errorf("Expected GetModuleRootPath to return module root path, got error: %v", err)
|
||||
return
|
||||
}
|
||||
persons, err := Load(rootPath + "/data/heros.json")
|
||||
if err != nil {
|
||||
t.Errorf("Expected Load to return persons, got error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Test count loaded persons
|
||||
if len(persons) < 1 {
|
||||
t.Errorf("Expexted at least one person, got none")
|
||||
}
|
||||
|
||||
// Test with valid
|
||||
if len(SearchByPowers(persons, []string{"healing"})) < 1 {
|
||||
t.Errorf("Expected at least one person with healing superpower, got none")
|
||||
}
|
||||
// Test with valid and invalid
|
||||
if len(SearchByPowers(persons, []string{"healing", "slowpoking"})) < 1 {
|
||||
t.Errorf("Expected at least one person with healing superpower, got none")
|
||||
}
|
||||
|
||||
// Test with invalid
|
||||
if len(SearchByPowers(persons, []string{"programming"})) > 1 {
|
||||
t.Errorf("Expected no one person with programming superpower, got some")
|
||||
}
|
||||
}
|
||||
124
pkg/deesee/model.go
Normal file
124
pkg/deesee/model.go
Normal file
@@ -0,0 +1,124 @@
|
||||
package deesee
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/eslider/superherohub/pkg/people"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// superPower is a type for superpowers.
|
||||
type superPower string
|
||||
|
||||
// List of allowed superpowers.
|
||||
const (
|
||||
strength superPower = "strength"
|
||||
speed superPower = "speed"
|
||||
flight superPower = "flight"
|
||||
invulnerability superPower = "invulnerability"
|
||||
healing superPower = "healing"
|
||||
)
|
||||
|
||||
// To keep the system as simple as possible,
|
||||
// DeeSee has agreed to only accept superheroes with the following superpowers.
|
||||
var allowedSuperPowers = [5]superPower{strength, speed, flight, invulnerability, healing}
|
||||
|
||||
// Superhero DeeSee person type.
|
||||
type Superhero people.Person
|
||||
|
||||
// IsAcceptable checks if the superhero is acceptable.
|
||||
func (s *Superhero) IsAcceptable() (r bool) {
|
||||
// Check if the person has superpowers.
|
||||
if *s.SuperPowers == nil {
|
||||
return false
|
||||
}
|
||||
// Check if the person has at least one acceptable superpower.
|
||||
for _, p := range *s.SuperPowers {
|
||||
if IsAcceptable(p) {
|
||||
r = true
|
||||
break
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// IsSuperHero checks if the superhero is acceptable.
|
||||
func (s *Superhero) IsSuperHero() bool {
|
||||
return s.IsAcceptable()
|
||||
}
|
||||
|
||||
// Has checks if the superhero has the given power.
|
||||
func (s *Superhero) Has(power string) bool {
|
||||
return s.SuperPowers.Contains(power)
|
||||
}
|
||||
|
||||
// IsAcceptable checks if the given power is allowed.
|
||||
func IsAcceptable(power string) bool {
|
||||
for _, p := range allowedSuperPowers {
|
||||
if string(p) == power {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Load JSON file and decode to `[]*Person` list
|
||||
func Load(path string) (l []*Superhero, err error) {
|
||||
var f *os.File
|
||||
if f, err = os.Open(path); err != nil {
|
||||
return
|
||||
}
|
||||
// Parse the request body
|
||||
err = json.NewDecoder(f).Decode(&l)
|
||||
return
|
||||
}
|
||||
|
||||
// EncryptHerosIdentities encrypts all persons identities with DeeSee encryption algorithm
|
||||
func EncryptHerosIdentities(heros []*Superhero, key int) (r []*Superhero) {
|
||||
for _, person := range heros {
|
||||
p := &Superhero{
|
||||
Name: person.Name,
|
||||
Identity: EncryptIdentity(person.Identity, key),
|
||||
SuperPowers: person.SuperPowers,
|
||||
Birthday: person.Birthday,
|
||||
}
|
||||
r = append(r, p)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// EncryptIdentity encrypts person identity with DeeSee encryption algorithm
|
||||
func EncryptIdentity(identity *people.Identity, key int) *people.Identity {
|
||||
return &people.Identity{
|
||||
FirstName: Encrypt(identity.FirstName, key),
|
||||
LastName: Encrypt(identity.LastName, key),
|
||||
}
|
||||
}
|
||||
|
||||
// SearchByPowers from persons list
|
||||
func SearchByPowers(heros []*Superhero, powers []string) (r []*Superhero) {
|
||||
for _, hero := range heros {
|
||||
for _, power := range powers {
|
||||
if hero.IsSuperHero() && hero.Has(power) {
|
||||
r = append(r, hero)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// NewSuperhero creates a new superhero
|
||||
func NewSuperhero() *Superhero {
|
||||
return &Superhero{}
|
||||
}
|
||||
|
||||
// FindByName from superheros list
|
||||
func FindByName(heros []*Superhero, name string) *Superhero {
|
||||
for _, hero := range heros {
|
||||
if strings.EqualFold(name, hero.Name) {
|
||||
return hero
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
45
pkg/file/utils.go
Normal file
45
pkg/file/utils.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package file
|
||||
|
||||
// This package makes it easier to implement tests to load data from a files.
|
||||
//
|
||||
// NOTE: Although it was not explicitly mentioned in the task,
|
||||
// I decided to make the GetModRootPath() function return path and an error if `go.mod` was not found.
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
// GetModRootPath get the root path of the module
|
||||
func GetModRootPath() (string, error) {
|
||||
// Get caller filePath
|
||||
_, path, _, _ := runtime.Caller(0)
|
||||
// Get base directory by file info
|
||||
var prevPath string
|
||||
for {
|
||||
path = filepath.Dir(path)
|
||||
|
||||
if IsExist(path + "/go.mod") {
|
||||
return path, nil
|
||||
}
|
||||
// Break if we reach root directory: '/'
|
||||
// Break if prevPath is same, maybe on Windows, something like C:// or c:\\
|
||||
if prevPath == path || len(path) < 2 {
|
||||
break
|
||||
}
|
||||
|
||||
prevPath = path
|
||||
}
|
||||
return "", errors.New("go.mod not found")
|
||||
}
|
||||
|
||||
// IsExist the path and isn't directory
|
||||
func IsExist(path string) bool {
|
||||
info, err := os.Stat(path)
|
||||
if os.IsNotExist(err) {
|
||||
return false
|
||||
}
|
||||
return !info.IsDir()
|
||||
}
|
||||
37
pkg/people/birthday.go
Normal file
37
pkg/people/birthday.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package people
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Declaring birthday layout constant
|
||||
const birthdayLayout = "2006-01-02" // YYYY-MM-DD - is default birthday layout
|
||||
|
||||
// Birthday is a custom encapsulation type for time.Time to handle birthdays
|
||||
// by decoding and encoding from and to JSON format
|
||||
type Birthday struct {
|
||||
time.Time // Embedding time.Time
|
||||
err error // Time parsing error
|
||||
}
|
||||
|
||||
// UnmarshalJSON decodes a date text to time.Time object
|
||||
func (t *Birthday) UnmarshalJSON(bytes []byte) (err error) {
|
||||
// 1. Decode the bytes into an int64
|
||||
// 2. Parse the unix timestamp
|
||||
|
||||
// Is value quoted?
|
||||
if rune(bytes[0]) == '"' || rune(bytes[0]) == '\'' {
|
||||
bytes = bytes[1 : len(bytes)-1] // remove quotes
|
||||
}
|
||||
|
||||
// Parse string to time.Time
|
||||
t.Time, t.err = time.Parse(birthdayLayout, string(bytes))
|
||||
t.err = err
|
||||
return
|
||||
}
|
||||
|
||||
// MarshalJSON encodes a time.Time object to date JSON text
|
||||
func (t Birthday) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(t.Time.Format(birthdayLayout))
|
||||
}
|
||||
7
pkg/people/identity.go
Normal file
7
pkg/people/identity.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package people
|
||||
|
||||
// Identity represents the true identity of a person
|
||||
type Identity struct {
|
||||
FirstName string `json:"firstName" deesee:"encode"`
|
||||
LastName string `json:"lastName" deesee:"encode"`
|
||||
}
|
||||
76
pkg/people/person.go
Normal file
76
pkg/people/person.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package people
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
// Person data representation.
|
||||
//
|
||||
// NOTE:
|
||||
//
|
||||
// The reason the people package was created is reusability.
|
||||
//
|
||||
// Personalities are used more often and are more general
|
||||
// in applications than superheroes.
|
||||
//
|
||||
// # Some additional reflection:
|
||||
//
|
||||
// * Superheroes are a special case of people.
|
||||
// * Normal persons are not superheros at all, but:
|
||||
// - An ordinary person, over time, can gain superpowers
|
||||
// - Do we need to handle them as no superhero?
|
||||
// - A superhero can lose his superpowers.
|
||||
// - But he was already using them, so does that mean he's no longer a superhero?
|
||||
// - Batman, as example has, no superpowers, so he is not a DeeSee "superhero".
|
||||
//
|
||||
// Additional to think about, is that People, a man,
|
||||
// can have many personalities(natural, juridical, fantasy, artistic, fake),
|
||||
// also superheros can become transformation(magically) to another person(becomes additional personalities),
|
||||
// so the person becomes more superpowers? (rhetorical question)
|
||||
//
|
||||
// .
|
||||
// Anyway, to work anything about, there is a `Person` a struct which has a Method `IsSuperHero() bool.
|
||||
type Person struct {
|
||||
// Name of a Person
|
||||
Name string `json:"name"`
|
||||
|
||||
// Identity of a Person.
|
||||
// Sure, people can have many identities, fakes too.
|
||||
// But at the time we handle only one-to-one person to identity relation
|
||||
Identity *Identity `json:"identity"`
|
||||
|
||||
// SuperPowers list
|
||||
SuperPowers *SuperPowers `json:"superpowers"`
|
||||
|
||||
// Birthday formatted as `YYYY-MM-DD` text string, there is no time zone and time.
|
||||
// The format-Layout is defined in internal `birthdayLayout` constant
|
||||
Birthday *Birthday `json:"birthday"`
|
||||
}
|
||||
|
||||
// IsSuperHero the person?
|
||||
func (p *Person) IsSuperHero() bool {
|
||||
return p.SuperPowers != nil && len(*p.SuperPowers) > 0
|
||||
}
|
||||
|
||||
// Has person the superpower?
|
||||
// In other words: is the person a superhero at all?
|
||||
func (p *Person) Has(power string) bool {
|
||||
// The excessive use of the IsSuperHero() method is intentional, but for clarity,
|
||||
// because it better describes what's going on.
|
||||
//
|
||||
// In the real world we deal with optimization of methods
|
||||
// and of course it's better not to do so...
|
||||
return p.IsSuperHero() && p.SuperPowers.Contains(power)
|
||||
}
|
||||
|
||||
// Marshal marshals the person to JSON
|
||||
func (p *Person) Marshal() ([]byte, error) {
|
||||
return json.Marshal(p)
|
||||
|
||||
}
|
||||
|
||||
func Unmarshal(js string) (p *Person, err error) {
|
||||
p = &Person{}
|
||||
err = json.Unmarshal([]byte(js), p)
|
||||
return
|
||||
}
|
||||
48
pkg/people/person_test.go
Normal file
48
pkg/people/person_test.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package people
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestPersonModel tests the Unmarshalling and properties.
|
||||
func TestPersonModel(t *testing.T) {
|
||||
p, err := Unmarshal(`{"name": "batman","identity": {"firstName": "bruce","lastName": "wayne"},"birthday": "1915-04-17"}`)
|
||||
if err != nil {
|
||||
t.Fatalf("Unmarshal() error = %v", err)
|
||||
}
|
||||
|
||||
if p.Name != "batman" ||
|
||||
p.Identity.FirstName != "bruce" ||
|
||||
p.Identity.LastName != "wayne" ||
|
||||
p.Birthday.Year() != 1915 {
|
||||
t.Fatalf("Unmarshal() error")
|
||||
}
|
||||
|
||||
if p.IsSuperHero() {
|
||||
t.Fatalf("Batman has no superpowers")
|
||||
}
|
||||
|
||||
js, err := p.Marshal()
|
||||
if err != nil {
|
||||
t.Fatalf("Marshal() error = %v", err)
|
||||
}
|
||||
|
||||
if !strings.Contains(string(js), `"birthday":"1915-04-17"`) {
|
||||
t.Fatalf("Marshal() birthday error")
|
||||
}
|
||||
|
||||
// Batman with superpowers
|
||||
p, err = Unmarshal(`{"name": "batman","identity": {"firstName": "bruce","lastName": "wayne"},"birthday": "1915-04-17","superpowers": ["money","intelligence"]}`)
|
||||
if err != nil {
|
||||
t.Fatalf("Unmarshal() error = %v", err)
|
||||
}
|
||||
|
||||
if !p.IsSuperHero() || !p.Has("money") || !p.Has("intelligence") {
|
||||
t.Fatalf("Batman has some superpowers as well")
|
||||
}
|
||||
|
||||
if p.Has("flight") {
|
||||
t.Fatalf("Batman has no superpowers")
|
||||
}
|
||||
}
|
||||
19
pkg/people/power.go
Normal file
19
pkg/people/power.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package people
|
||||
|
||||
import "strings"
|
||||
|
||||
// SuperPowers list of an SuperHero.
|
||||
// Normal person has no super-power?
|
||||
// Perhaps we just don't know enough to distinguish the qualities in each individual person. Sad but true.
|
||||
type SuperPowers []string
|
||||
|
||||
// Contains superpower list the strength?
|
||||
// Not go idiomatic? Okay, we can discuss it.
|
||||
func (s *SuperPowers) Contains(power string) bool {
|
||||
for _, p := range *s {
|
||||
if strings.EqualFold(p, power) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
Reference in New Issue
Block a user