Add initial project files

This commit is contained in:
2023-02-07 04:44:07 +00:00
commit af7291815d
27 changed files with 1415 additions and 0 deletions

75
pkg/deesee/codec.go Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
}