mirror of
https://gitlab.com/djdietrick/docs
synced 2026-05-03 03:50:54 -04:00
Compare commits
16 Commits
15b2440d19
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| c601784b5c | |||
| cfaeadf198 | |||
| 6ff585e133 | |||
| f619093d76 | |||
| c2366ac927 | |||
| 65ead5a6f2 | |||
| f69fcfc50b | |||
| 2644b07951 | |||
| 9458db3381 | |||
| 894a15525a | |||
| 2b8ca93fab | |||
| d14a1a05c8 | |||
| 8bd8196841 | |||
| 6dc2e294b4 | |||
| 5ae50d9381 | |||
| 2c05f038da |
8
docs/.vitepress/cache/deps/_metadata.json
vendored
8
docs/.vitepress/cache/deps/_metadata.json
vendored
@@ -7,25 +7,25 @@
|
||||
"vue": {
|
||||
"src": "../../../../node_modules/vue/dist/vue.runtime.esm-bundler.js",
|
||||
"file": "vue.js",
|
||||
"fileHash": "9f6650f2",
|
||||
"fileHash": "6eb7142a",
|
||||
"needsInterop": false
|
||||
},
|
||||
"vitepress > @vue/devtools-api": {
|
||||
"src": "../../../../node_modules/@vue/devtools-api/dist/index.js",
|
||||
"file": "vitepress___@vue_devtools-api.js",
|
||||
"fileHash": "87d887f1",
|
||||
"fileHash": "f77c0dac",
|
||||
"needsInterop": false
|
||||
},
|
||||
"vitepress > @vueuse/core": {
|
||||
"src": "../../../../node_modules/@vueuse/core/index.mjs",
|
||||
"file": "vitepress___@vueuse_core.js",
|
||||
"fileHash": "3179a954",
|
||||
"fileHash": "4488c276",
|
||||
"needsInterop": false
|
||||
},
|
||||
"@theme/index": {
|
||||
"src": "../../../../node_modules/vitepress/dist/client/theme-default/index.js",
|
||||
"file": "@theme_index.js",
|
||||
"fileHash": "1bd77338",
|
||||
"fileHash": "f16ffef2",
|
||||
"needsInterop": false
|
||||
}
|
||||
},
|
||||
|
||||
@@ -16,6 +16,9 @@ export default {
|
||||
"/server/": require("../server/sidebar.json"),
|
||||
"/aws/": require("../aws/sidebar.json"),
|
||||
"/go": require("../go/sidebar.json"),
|
||||
"/react": require("../react/sidebar.json"),
|
||||
"/vue": require("../vue/sidebar.json"),
|
||||
"/java": require("../java/sidebar.json"),
|
||||
"/": [
|
||||
{
|
||||
text: "Home",
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# Frameworks
|
||||
|
||||
[Nuxtjs](/nuxt/)
|
||||
|
||||
[React](/react/)
|
||||
|
||||
41
docs/go/advanced/context.md
Normal file
41
docs/go/advanced/context.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# Context
|
||||
|
||||
Context is a way of passing information or enforcing timeouts when calling functions throughout your application as a single requests goes from layer to layer. This is usually passed as the first parameter in your functions if being used.
|
||||
|
||||
```go
|
||||
// create an empty context
|
||||
ctx := context.Background()
|
||||
|
||||
// append value to context
|
||||
ctx = context.WithValue(ctx, "key", "value")
|
||||
|
||||
value := ctx.Value("key")
|
||||
```
|
||||
|
||||
## Timeouts
|
||||
|
||||
```go
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2 * time.Second)
|
||||
|
||||
defer cancel() // can call before timeout
|
||||
go longRunningFunc(ctx)
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
fms.Println("done blocking main thread also!")
|
||||
}
|
||||
time.Sleep(2*time.Second)
|
||||
|
||||
func longRunningFunc(ctx context.Context) {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
fmt.Println("timed out")
|
||||
return
|
||||
default:
|
||||
fmt.Println("still processing")
|
||||
}
|
||||
time.Sleep(1*time.Second)
|
||||
}
|
||||
}
|
||||
```
|
||||
555
docs/go/advanced/db.md
Normal file
555
docs/go/advanced/db.md
Normal file
@@ -0,0 +1,555 @@
|
||||
# Databases
|
||||
|
||||
## Postgres
|
||||
|
||||
```bash
|
||||
go get github.com/jackc/pgconn
|
||||
go get github.com/jackc/pgx/v4
|
||||
go get github.com/jackc/pgx/v4/stdlib
|
||||
```
|
||||
|
||||
```go
|
||||
// models.go
|
||||
package data
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
const dbTimeout = time.Second * 3
|
||||
|
||||
var db *sql.DB
|
||||
|
||||
type PostgresRepository struct {
|
||||
Conn *sql.DB
|
||||
}
|
||||
|
||||
func NewPostgresRepository(conn *sql.DB) *PostgresRepository {
|
||||
db = conn
|
||||
return &PostgresRepository{
|
||||
Conn: conn,
|
||||
}
|
||||
}
|
||||
|
||||
type User struct {
|
||||
ID int `json:"id"`
|
||||
Email string `json:"email"`
|
||||
FirstName string `json:"first_name,omitempty"`
|
||||
LastName string `json:"last_name,omitempty"`
|
||||
Password string `json:"-"`
|
||||
Active int `json:"active"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// GetAll returns a slice of all users, sorted by last name
|
||||
func (u *PostgresRepository) GetAll() ([]*User, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), dbTimeout)
|
||||
defer cancel()
|
||||
|
||||
query := `select id, email, first_name, last_name, password, user_active, created_at, updated_at
|
||||
from users order by last_name`
|
||||
|
||||
rows, err := u.Conn.QueryContext(ctx, query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var users []*User
|
||||
|
||||
for rows.Next() {
|
||||
var user User
|
||||
err := rows.Scan(
|
||||
&user.ID,
|
||||
&user.Email,
|
||||
&user.FirstName,
|
||||
&user.LastName,
|
||||
&user.Password,
|
||||
&user.Active,
|
||||
&user.CreatedAt,
|
||||
&user.UpdatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
log.Println("Error scanning", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
users = append(users, &user)
|
||||
}
|
||||
|
||||
return users, nil
|
||||
}
|
||||
|
||||
// GetByEmail returns one user by email
|
||||
func (u *PostgresRepository) GetByEmail(email string) (*User, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), dbTimeout)
|
||||
defer cancel()
|
||||
|
||||
query := `select id, email, first_name, last_name, password, user_active, created_at, updated_at from users where email = $1`
|
||||
|
||||
var user User
|
||||
row := db.QueryRowContext(ctx, query, email)
|
||||
|
||||
err := row.Scan(
|
||||
&user.ID,
|
||||
&user.Email,
|
||||
&user.FirstName,
|
||||
&user.LastName,
|
||||
&user.Password,
|
||||
&user.Active,
|
||||
&user.CreatedAt,
|
||||
&user.UpdatedAt,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// GetOne returns one user by id
|
||||
func (u *PostgresRepository) GetOne(id int) (*User, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), dbTimeout)
|
||||
defer cancel()
|
||||
|
||||
query := `select id, email, first_name, last_name, password, user_active, created_at, updated_at from users where id = $1`
|
||||
|
||||
var user User
|
||||
row := db.QueryRowContext(ctx, query, id)
|
||||
|
||||
err := row.Scan(
|
||||
&user.ID,
|
||||
&user.Email,
|
||||
&user.FirstName,
|
||||
&user.LastName,
|
||||
&user.Password,
|
||||
&user.Active,
|
||||
&user.CreatedAt,
|
||||
&user.UpdatedAt,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// Update updates one user in the database, using the information
|
||||
// stored in the receiver u
|
||||
func (u *PostgresRepository) Update(user User) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), dbTimeout)
|
||||
defer cancel()
|
||||
|
||||
stmt := `update users set
|
||||
email = $1,
|
||||
first_name = $2,
|
||||
last_name = $3,
|
||||
user_active = $4,
|
||||
updated_at = $5
|
||||
where id = $6
|
||||
`
|
||||
|
||||
_, err := db.ExecContext(ctx, stmt,
|
||||
user.Email,
|
||||
user.FirstName,
|
||||
user.LastName,
|
||||
user.Active,
|
||||
time.Now(),
|
||||
user.ID,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteByID deletes one user from the database, by ID
|
||||
func (u *PostgresRepository) DeleteByID(id int) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), dbTimeout)
|
||||
defer cancel()
|
||||
|
||||
stmt := `delete from users where id = $1`
|
||||
|
||||
_, err := db.ExecContext(ctx, stmt, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Insert inserts a new user into the database, and returns the ID of the newly inserted row
|
||||
func (u *PostgresRepository) Insert(user User) (int, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), dbTimeout)
|
||||
defer cancel()
|
||||
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(user.Password), 12)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
var newID int
|
||||
stmt := `insert into users (email, first_name, last_name, password, user_active, created_at, updated_at)
|
||||
values ($1, $2, $3, $4, $5, $6, $7) returning id`
|
||||
|
||||
err = db.QueryRowContext(ctx, stmt,
|
||||
user.Email,
|
||||
user.FirstName,
|
||||
user.LastName,
|
||||
hashedPassword,
|
||||
user.Active,
|
||||
time.Now(),
|
||||
time.Now(),
|
||||
).Scan(&newID)
|
||||
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return newID, nil
|
||||
}
|
||||
```
|
||||
|
||||
```go
|
||||
// main.go, instantiate connection and repo
|
||||
package main
|
||||
|
||||
import (
|
||||
"authentication/data"
|
||||
|
||||
_ "github.com/jackc/pgconn"
|
||||
_ "github.com/jackc/pgx/v4"
|
||||
_ "github.com/jackc/pgx/v4/stdlib"
|
||||
)
|
||||
func connectToDB() *sql.DB {
|
||||
dsn := os.Getenv("DSN")
|
||||
|
||||
for {
|
||||
conn, err := openDB(dsn)
|
||||
if err != nil {
|
||||
log.Println("DB not ready yet...")
|
||||
counts++
|
||||
} else {
|
||||
log.Println("Connected to DB!")
|
||||
return conn
|
||||
}
|
||||
if counts > 10 {
|
||||
log.Println(err)
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Println("Backing off for 2 seconds...")
|
||||
time.Sleep(time.Second * 2)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
func (app *Config) setupRepo(conn *sql.DB) {
|
||||
db := data.NewPostgresRepository(conn)
|
||||
app.Repo = db
|
||||
}
|
||||
```
|
||||
|
||||
### Testing
|
||||
|
||||
It is best to use a repository pattern when dealing with databases, because it allows you to implement a mock repo that implements all the same functions as the real repo with mock return values.
|
||||
|
||||
```go
|
||||
// repository.go
|
||||
package data
|
||||
|
||||
type Repository interface {
|
||||
GetAll() ([]*User, error)
|
||||
GetByEmail(email string) (*User, error)
|
||||
GetOne(id int) (*User, error)
|
||||
Insert(user User) (int, error)
|
||||
Update(user User) error
|
||||
DeleteByID(id int) error
|
||||
ResetPassword(password string, user User) error
|
||||
PasswordMatches(password string, user User) (bool, error)
|
||||
}
|
||||
```
|
||||
|
||||
```go
|
||||
package data
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"time"
|
||||
)
|
||||
|
||||
type PostgresTestRepository struct {
|
||||
Conn *sql.DB
|
||||
}
|
||||
|
||||
func NewPostgresTestRepository(conn *sql.DB) *PostgresTestRepository {
|
||||
return &PostgresTestRepository{
|
||||
Conn: conn,
|
||||
}
|
||||
}
|
||||
|
||||
// Implement all methods of Repository interface
|
||||
func (repo *PostgresTestRepository) GetAll() ([]*User, error) {
|
||||
users := []*User{}
|
||||
return users, nil
|
||||
}
|
||||
```
|
||||
|
||||
```go
|
||||
// setup_test.go
|
||||
package main
|
||||
|
||||
import (
|
||||
"authentication/data"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var testApp Config
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
repo := data.NewPostgresTestRepository(nil)
|
||||
testApp.Repo = repo
|
||||
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
```
|
||||
|
||||
## Mongo
|
||||
|
||||
```bash
|
||||
go get go.mongodb.org/mongo-driver/mongo
|
||||
go get go.mongodb.org/mongo-driver/mongo/options
|
||||
```
|
||||
|
||||
```go
|
||||
// models.go
|
||||
package data
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
var client *mongo.Client
|
||||
|
||||
func New(mongo *mongo.Client) Models {
|
||||
client = mongo
|
||||
|
||||
return Models{
|
||||
LogEntry: LogEntry{},
|
||||
}
|
||||
}
|
||||
|
||||
type Models struct {
|
||||
LogEntry LogEntry
|
||||
}
|
||||
|
||||
type LogEntry struct {
|
||||
ID string `bson:"_id,omitempty" json:"id,omitempty"`
|
||||
Name string `bson:"name" json:"name"`
|
||||
Data string `bson:"data" json:"data"`
|
||||
CreatedAt time.Time `bson:"created_at" json:"created_at"`
|
||||
UpdatedAt time.Time `bson:"updated_at" json:"updated_at"`
|
||||
}
|
||||
|
||||
func (l *LogEntry) Insert(entry LogEntry) error {
|
||||
collection := client.Database("logs").Collection("logs")
|
||||
|
||||
_, err := collection.InsertOne(context.TODO(), LogEntry{
|
||||
Name: entry.Name,
|
||||
Data: entry.Data,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
})
|
||||
if err != nil {
|
||||
log.Println("Error insertint into logs: ", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *LogEntry) All() ([]*LogEntry, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
|
||||
collection := client.Database("logs").Collection("logs")
|
||||
|
||||
opts := options.Find()
|
||||
opts.SetSort(bson.D{{"created_at", -1}})
|
||||
|
||||
cursor, err := collection.Find(context.TODO(), bson.D{}, opts)
|
||||
if err != nil {
|
||||
log.Println("Finding all docs error: ", err)
|
||||
return nil, err
|
||||
}
|
||||
defer cursor.Close(ctx)
|
||||
|
||||
var logs []*LogEntry
|
||||
|
||||
for cursor.Next(ctx) {
|
||||
var item LogEntry
|
||||
|
||||
err := cursor.Decode(&item)
|
||||
if err != nil {
|
||||
log.Println("Error decoding log into slice: ", err)
|
||||
return nil, err
|
||||
} else {
|
||||
logs = append(logs, &item)
|
||||
}
|
||||
}
|
||||
|
||||
return logs, nil
|
||||
}
|
||||
|
||||
func (l *LogEntry) GetOne(id string) (*LogEntry, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
|
||||
collection := client.Database("logs").Collection("logs")
|
||||
|
||||
docId, err := primitive.ObjectIDFromHex(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var entry LogEntry
|
||||
err = collection.FindOne(ctx, bson.M{"_id": docId}).Decode(&entry)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &entry, nil
|
||||
}
|
||||
|
||||
func (l *LogEntry) DropCollections() error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
|
||||
collection := client.Database("logs").Collection("logs")
|
||||
|
||||
if err := collection.Drop(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *LogEntry) Update() (*mongo.UpdateResult, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
|
||||
collection := client.Database("logs").Collection("logs")
|
||||
docId, err := primitive.ObjectIDFromHex(l.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result, err := collection.UpdateOne(
|
||||
ctx,
|
||||
bson.M{"_id": docId},
|
||||
bson.D{
|
||||
{"$set", bson.D{
|
||||
{"name", l.Name},
|
||||
{"data", l.Data},
|
||||
{"updated_at", time.Now()},
|
||||
}},
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return result, err
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
```go
|
||||
// main.go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"logger/data"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/rpc"
|
||||
"time"
|
||||
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
const (
|
||||
mongoURL = "mongodb://mongo:27017"
|
||||
)
|
||||
|
||||
var client *mongo.Client
|
||||
|
||||
type Config struct {
|
||||
Models data.Models
|
||||
}
|
||||
|
||||
func main() {
|
||||
// connect to mongo
|
||||
mongoClient, err := connectToMongo()
|
||||
if err != nil {
|
||||
log.Panic(err)
|
||||
}
|
||||
|
||||
client = mongoClient
|
||||
|
||||
// create a context to disconnect
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// close connection
|
||||
defer func() {
|
||||
if err = client.Disconnect(ctx); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}()
|
||||
|
||||
app := Config{
|
||||
Models: data.New(client),
|
||||
}
|
||||
}
|
||||
|
||||
func connectToMongo() (*mongo.Client, error) {
|
||||
// create connection options
|
||||
clientOptions := options.Client().ApplyURI(mongoURL)
|
||||
clientOptions.SetAuth(options.Credential{
|
||||
Username: "admin",
|
||||
Password: "password",
|
||||
})
|
||||
|
||||
c, err := mongo.Connect(context.TODO(), clientOptions)
|
||||
if err != nil {
|
||||
log.Println("Error connecting: %s", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
```
|
||||
62
docs/go/advanced/flags.md
Normal file
62
docs/go/advanced/flags.md
Normal file
@@ -0,0 +1,62 @@
|
||||
# Flags
|
||||
|
||||
Flags allow you to define command line arguments to your applications. This gives you some more useful features over `os.Args` such as help strings and validation.
|
||||
|
||||
```go
|
||||
var age = flag.Int("age", 20, "age of user") // arg name, default, help
|
||||
fmt.Println(*age) // gives us a pointer to the value
|
||||
// or
|
||||
var age int
|
||||
flag.IntVar(&age, "age", 20, "age of user")
|
||||
fmt.Println(age)
|
||||
|
||||
// with structs
|
||||
type ServerConfig struct {
|
||||
port int
|
||||
env string
|
||||
}
|
||||
|
||||
var config ServerConfig
|
||||
flag.IntVar(&config.port, "port", 8000, "server port")
|
||||
flag.StringVar(&config.env, "env", "dev", "environment")
|
||||
flag.Parse()
|
||||
```
|
||||
|
||||
## Custom Flags
|
||||
|
||||
Normally flags can only be strings, ints, or booleans, but you can create your own custom flag types by implementing the `Value` interface which has two functions, `String` and `Set`.
|
||||
|
||||
```go
|
||||
type AddrVal struct {
|
||||
addr *string
|
||||
}
|
||||
|
||||
func (a AddrVal) String() string {
|
||||
if a.addr == nil {
|
||||
return ""
|
||||
}
|
||||
return *a.addr
|
||||
}
|
||||
|
||||
func (a AddrVal) Set(s string) error {
|
||||
if err := validateAddr(s); err != nil {
|
||||
return err
|
||||
}
|
||||
*a.addr = s
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateAddr(addr string) error {
|
||||
// validate address
|
||||
return nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
addr := ":8080" // default value
|
||||
flag.Var(AddrVal{&addr}, "addr", "address to listen on")
|
||||
flag.Parse()
|
||||
|
||||
fmt.Println("address: ", addr)
|
||||
}
|
||||
|
||||
```
|
||||
212
docs/go/advanced/grpc.md
Normal file
212
docs/go/advanced/grpc.md
Normal file
@@ -0,0 +1,212 @@
|
||||
# gRPC
|
||||
|
||||
GRPC is an enhancement to RPC in that both sides don't have to be written in Go, because there are libraries for a lot of other languages. It also defines it's payload types in separate proto files so they can be shared among teams.
|
||||
|
||||
Documentation: https://grpc.io/docs/languages/go/basics/
|
||||
|
||||
## Install GRPC
|
||||
|
||||
```bash
|
||||
go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.27
|
||||
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.2
|
||||
```
|
||||
|
||||
## Proto Files
|
||||
|
||||
```proto
|
||||
// log.proto
|
||||
syntax = "proto3";
|
||||
|
||||
package logs;
|
||||
|
||||
option go_package = "/logs";
|
||||
|
||||
message Log{
|
||||
string name = 1;
|
||||
string data = 2;
|
||||
}
|
||||
|
||||
message LogRequest{
|
||||
Log logEntry = 1;
|
||||
}
|
||||
|
||||
message LogResponse{
|
||||
string result = 1;
|
||||
}
|
||||
|
||||
service LogService{
|
||||
rpc WriteLog(LogRequest) returns (LogResponse);
|
||||
}
|
||||
```
|
||||
|
||||
## Compile Protos
|
||||
|
||||
Proto compiler link: https://grpc.io/docs/protoc-installation/. This should be stored somewhere in your path, preferably in your `~/go/bin` directory.
|
||||
|
||||
In directory with your proto file, run the below command. This will generate
|
||||
|
||||
```bash
|
||||
protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative logs.proto
|
||||
```
|
||||
|
||||
## Listener
|
||||
|
||||
### Install dependencies
|
||||
|
||||
```bash
|
||||
go get google.golang.org/gprc
|
||||
go get google.golang.org/protobuf
|
||||
```
|
||||
|
||||
### Implement handlers
|
||||
|
||||
```go
|
||||
// grpc.go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"logger/data" // our db models
|
||||
"logger/logs" // our proto files
|
||||
"net"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
type LogServer struct {
|
||||
logs.UnimplementedLogServiceServer
|
||||
Models data.Models // db writer
|
||||
}
|
||||
|
||||
// Using code generated from proto
|
||||
func (l *LogServer) WriteLog(ctx context.Context, req *logs.LogRequest) (*logs.LogResponse, error) {
|
||||
input := req.GetLogEntry() // auto generated function
|
||||
|
||||
logEntry := data.LogEntry{
|
||||
Name: input.Name,
|
||||
Data: input.Data,
|
||||
}
|
||||
|
||||
err := l.Models.LogEntry.Insert(logEntry)
|
||||
if err != nil {
|
||||
resp := &logs.LogResponse{
|
||||
Result: "failed",
|
||||
}
|
||||
return resp, err
|
||||
}
|
||||
|
||||
resp := &logs.LogResponse{
|
||||
Result: "logged!",
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (app *Config) gRPCListen() {
|
||||
lis, err := net.Listen("tcp", fmt.Sprintf(":%s", grpcPort))
|
||||
if err != nil {
|
||||
log.Fatalf("failed to listen to gRPC: %v", err)
|
||||
}
|
||||
|
||||
s := grpc.NewServer()
|
||||
|
||||
logs.RegisterLogServiceServer(s, &LogServer{
|
||||
Models: app.Models,
|
||||
})
|
||||
|
||||
log.Printf("gRPC server listening on port %s", grpcPort)
|
||||
|
||||
if err := s.Serve(lis); err != nil {
|
||||
log.Fatalf("failed to serve gRPC: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Instantiate server
|
||||
|
||||
```go
|
||||
// grpc.go
|
||||
func (app *Config) gRPCListen() {
|
||||
lis, err := net.Listen("tcp", fmt.Sprintf(":%s", grpcPort))
|
||||
if err != nil {
|
||||
log.Fatalf("failed to listen to gRPC: %v", err)
|
||||
}
|
||||
|
||||
s := grpc.NewServer()
|
||||
|
||||
// auto generated function
|
||||
logs.RegisterLogServiceServer(s, &LogServer{
|
||||
Models: app.Models,
|
||||
})
|
||||
|
||||
log.Printf("gRPC server listening on port %s", grpcPort)
|
||||
|
||||
if err := s.Serve(lis); err != nil {
|
||||
log.Fatalf("failed to serve gRPC: %v", err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```go
|
||||
// main.go
|
||||
go app.grpcLisen()
|
||||
```
|
||||
|
||||
## Sender
|
||||
|
||||
The sender must also have access to the proto files generated by the listener. It would be a good idea to store these in a separate repo so that multiple teams can access them from a central source of truth.
|
||||
|
||||
```go
|
||||
import (
|
||||
"context"
|
||||
"log" // proto
|
||||
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
)
|
||||
|
||||
func (app *Config) logItemViaGRPC(w http.ResponseWriter, r *http.Request) {
|
||||
var requestPayload RequestPayload
|
||||
|
||||
err := app.readJSON(w, r, &requestPayload)
|
||||
if err != nil {
|
||||
app.errorJSON(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
// need to pass some sort of credentials to the server
|
||||
conn, err := grpc.Dial("logger:50001", grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithBlock())
|
||||
if err != nil {
|
||||
app.errorJSON(w, err)
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// generated in proto
|
||||
client := logs.NewLogServiceClient(conn)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
|
||||
_, err = client.WriteLog(ctx, &logs.LogRequest{
|
||||
LogEntry: &logs.Log{
|
||||
Name: requestPayload.Log.Name,
|
||||
Data: requestPayload.Log.Data,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
app.errorJSON(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
payload := jsonResponse{
|
||||
Error: false,
|
||||
Message: "logged via gRPC",
|
||||
}
|
||||
|
||||
app.writeJSON(w, http.StatusAccepted, payload)
|
||||
}
|
||||
```
|
||||
357
docs/go/advanced/http.md
Normal file
357
docs/go/advanced/http.md
Normal file
@@ -0,0 +1,357 @@
|
||||
# HTTP
|
||||
|
||||
The below snippets are using the `chi` package for Go.
|
||||
|
||||
### To Install
|
||||
|
||||
```bash
|
||||
go get github.com/go-chi/chi/v5
|
||||
go get github.com/go-chi/chi/v5/middleware
|
||||
go get github.com/go-chi/cors
|
||||
```
|
||||
|
||||
## Basic Implementation
|
||||
|
||||
```go
|
||||
// main.go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
_ "github.com/jackc/pgconn"
|
||||
_ "github.com/jackc/pgx/v4"
|
||||
_ "github.com/jackc/pgx/v4/stdlib"
|
||||
)
|
||||
|
||||
const webPort = "80"
|
||||
|
||||
var counts int64
|
||||
|
||||
type Config struct {
|
||||
Client *http.Client
|
||||
}
|
||||
|
||||
func main() {
|
||||
app := Config{
|
||||
Client: &http.Client{},
|
||||
}
|
||||
|
||||
srv := &http.Server{
|
||||
Addr: fmt.Sprintf(":%s", webPort),
|
||||
Handler: app.routes(),
|
||||
}
|
||||
|
||||
err := srv.ListenAndServe()
|
||||
if err != nil {
|
||||
log.Panic(err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```go
|
||||
// routes.go
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/go-chi/chi/middleware"
|
||||
"github.com/go-chi/cors"
|
||||
)
|
||||
|
||||
func (app *Config) routes() http.Handler {
|
||||
mux := chi.NewRouter()
|
||||
|
||||
mux.Use(cors.Handler(cors.Options{
|
||||
AllowedOrigins: []string{"https://*", "http://*"},
|
||||
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
|
||||
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"},
|
||||
ExposedHeaders: []string{"Link"},
|
||||
AllowCredentials: true,
|
||||
MaxAge: 300,
|
||||
}))
|
||||
|
||||
mux.Use(middleware.Heartbeat("/ping"))
|
||||
|
||||
mux.Post("/authenticate", app.Authenticate)
|
||||
|
||||
return mux
|
||||
}
|
||||
```
|
||||
|
||||
```go
|
||||
// handlers.go
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func (app *Config) Authenticate(w http.ResponseWriter, r *http.Request) {
|
||||
var requestPayload struct {
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
err := app.readJSON(w, r, &requestPayload)
|
||||
if err != nil {
|
||||
app.errorJSON(w, err, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// validate user against db
|
||||
user, err := app.Repo.GetByEmail(requestPayload.Email)
|
||||
if err != nil {
|
||||
app.errorJSON(w, errors.New("invalid credentials"), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
valid, err := app.Repo.PasswordMatches(requestPayload.Password, *user)
|
||||
if err != nil || !valid {
|
||||
app.errorJSON(w, errors.New("invalid credentials"), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// log authentication
|
||||
err = app.logRequest("authentication", fmt.Sprintf("%s logged in", user.Email))
|
||||
if err != nil {
|
||||
app.errorJSON(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Println("Logged authentication request")
|
||||
|
||||
payload := jsonResponse{
|
||||
Error: false,
|
||||
Message: fmt.Sprintf("Logged in user %s", requestPayload.Email),
|
||||
Data: user,
|
||||
}
|
||||
|
||||
app.writeJSON(w, http.StatusAccepted, payload)
|
||||
}
|
||||
|
||||
func (app *Config) logRequest(name, data string) error {
|
||||
var entry struct {
|
||||
Name string `json:"name"`
|
||||
Data string `json:"data"`
|
||||
}
|
||||
|
||||
entry.Name = name
|
||||
entry.Data = data
|
||||
|
||||
jsonData, _ := json.MarshalIndent(entry, "", "\t")
|
||||
|
||||
logURL := "http://logger/log"
|
||||
|
||||
request, err := http.NewRequest("POST", logURL, bytes.NewBuffer(jsonData))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = app.Client.Do(request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
```go
|
||||
// helpers.go
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type jsonResponse struct {
|
||||
Error bool `json:"error"`
|
||||
Message string `json:"message"`
|
||||
Data any `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
func (app *Config) readJSON(w http.ResponseWriter, r *http.Request, data any) error {
|
||||
maxBytes := 1048576 // one MB
|
||||
|
||||
r.Body = http.MaxBytesReader(w, r.Body, int64(maxBytes))
|
||||
|
||||
dec := json.NewDecoder(r.Body)
|
||||
err := dec.Decode(data)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = dec.Decode(&struct{}{})
|
||||
if err != io.EOF {
|
||||
return errors.New("Body must have only a single json value")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (app *Config) writeJSON(w http.ResponseWriter, status int, data any, headers ...http.Header) error {
|
||||
out, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(headers) > 0 {
|
||||
for key, val := range headers[0] {
|
||||
w.Header()[key] = val
|
||||
}
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
_, err = w.Write(out)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (app *Config) errorJSON(w http.ResponseWriter, err error, status ...int) error {
|
||||
statusCode := http.StatusBadRequest
|
||||
|
||||
if len(status) > 0 {
|
||||
statusCode = status[0]
|
||||
}
|
||||
|
||||
var payload jsonResponse
|
||||
payload.Error = true
|
||||
payload.Message = err.Error()
|
||||
|
||||
return app.writeJSON(w, statusCode, payload)
|
||||
}
|
||||
```
|
||||
|
||||
## Adding Custom Types to Session
|
||||
|
||||
```go
|
||||
// main.go, before creating session
|
||||
gob.Register(data.User{})
|
||||
|
||||
// to add type to session
|
||||
app.Session.Put(r.Context(), "user", user) // user *data.User
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
```go
|
||||
// handlers_test.go
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type RoundTripFunc func(req *http.Request) *http.Response
|
||||
|
||||
func (f RoundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
return f(req), nil
|
||||
}
|
||||
|
||||
func NewTestClient(fn RoundTripFunc) *http.Client {
|
||||
return &http.Client{
|
||||
Transport: fn,
|
||||
}
|
||||
}
|
||||
|
||||
func Test_Authenticate(t *testing.T) {
|
||||
jsonToReturn := `
|
||||
{
|
||||
"error": false,
|
||||
"message": "some message"
|
||||
}
|
||||
`
|
||||
client := NewTestClient(func(req *http.Request) *http.Response {
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: io.NopCloser(bytes.NewBufferString(jsonToReturn)),
|
||||
Header: make(http.Header),
|
||||
}
|
||||
})
|
||||
testApp.Client = client
|
||||
|
||||
postBody := map[string]interface{}{
|
||||
"email": "me@here.com",
|
||||
"password": "verysecret",
|
||||
}
|
||||
|
||||
body, _ := json.Marshal(postBody)
|
||||
|
||||
req, _ := http.NewRequest("POST", "/authenticate", bytes.NewReader(body))
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
handler := http.HandlerFunc(testApp.Authenticate)
|
||||
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
if rr.Code != http.StatusAccepted {
|
||||
t.Errorf("handler returned wrong status code: got %v want %v",
|
||||
rr.Code, http.StatusAccepted)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```go
|
||||
// routes_test.go
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
)
|
||||
|
||||
func Test_routes_exist(t *testing.T) {
|
||||
testApp := Config{}
|
||||
|
||||
testRoutes := testApp.routes()
|
||||
chiRoutes := testRoutes.(chi.Router)
|
||||
|
||||
routes := []string{"/authenticate"}
|
||||
|
||||
for _, route := range routes {
|
||||
routeExists(t, chiRoutes, route)
|
||||
}
|
||||
}
|
||||
|
||||
func routeExists(t *testing.T, routes chi.Router, route string) {
|
||||
found := false
|
||||
_ = chi.Walk(routes, func(method string, foundRoute string, handler http.Handler, middlewares ...func(http.Handler) http.Handler) error {
|
||||
if route == foundRoute {
|
||||
found = true
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if !found {
|
||||
t.Errorf("route %s not found", route)
|
||||
}
|
||||
}
|
||||
```
|
||||
97
docs/go/advanced/rpc.md
Normal file
97
docs/go/advanced/rpc.md
Normal file
@@ -0,0 +1,97 @@
|
||||
# RPC
|
||||
|
||||
RPC stands for Remote Procedure Calls, and allows for external Go microservices to call functions in our application directly.
|
||||
|
||||
## Listener
|
||||
|
||||
To do this, you need to define a RPCServer dummy type, and payload to receive, and a function that gets a pointer to a response string and returns an error.
|
||||
|
||||
```go
|
||||
type RPCServer struct{}
|
||||
|
||||
type RPCPayload struct {
|
||||
Name string
|
||||
Data string
|
||||
}
|
||||
|
||||
func (r *RPCServer) LogInfo(payload RPCPayload, resp *string) error {
|
||||
collection := client.Database("logs").Collection("logs")
|
||||
_, err := collection.InsertOne(context.TODO(), data.LogEntry{
|
||||
Name: payload.Name,
|
||||
Data: payload.Data,
|
||||
})
|
||||
if err != nil {
|
||||
log.Println("error writing to mongo ", err)
|
||||
return err
|
||||
}
|
||||
*resp = "processed payload via RPC: " + payload.Name
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
To instantiate the RPCServer, use the below syntax in your main application file. Basically you define a function that creates the connection and listens on a port. Then, you register your RPC handler type and serve the RPC server.
|
||||
|
||||
```go
|
||||
func (app *Config) rpcListen() error {
|
||||
log.Println("Starting RPC server on port " + RPC_PORT)
|
||||
listen, err := net.Listen("tcp", fmt.Sprintf("0.0.0.0:%s", RPC_PORT))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer listen.Close()
|
||||
|
||||
for {
|
||||
rpcConn, err := listen.Accept()
|
||||
if err != nil {
|
||||
log.Println("Error accepting connection: ", err)
|
||||
continue
|
||||
}
|
||||
go rpc.ServeConn(rpcConn)
|
||||
}
|
||||
}
|
||||
|
||||
// in function where you start your app...
|
||||
err = rpc.Register(new(RPCServer))
|
||||
go app.rpcListen()
|
||||
```
|
||||
|
||||
## Sender
|
||||
|
||||
To send messages via RPC, you need to create a type for the payload that exactly matches that of the receiver.
|
||||
|
||||
```go
|
||||
type RPCPayload struct {
|
||||
Name string
|
||||
Data string
|
||||
}
|
||||
|
||||
func (app *Config) logItemViaRPC(r http.ResponseWriter, l LogPayload) {
|
||||
client, err := rpc.Dial("tcp", "logger:5001")
|
||||
if err != nil {
|
||||
app.errorJSON(r, err)
|
||||
return
|
||||
}
|
||||
|
||||
payload := RPCPayload{
|
||||
Name: l.Name,
|
||||
Data: l.Data,
|
||||
}
|
||||
|
||||
var resp string
|
||||
|
||||
err = client.Call("RPCServer.LogInfo", payload, &resp)
|
||||
if err != nil {
|
||||
app.errorJSON(r, err)
|
||||
return
|
||||
}
|
||||
|
||||
returnPayload := jsonResponse{
|
||||
Error: false,
|
||||
Message: resp,
|
||||
}
|
||||
|
||||
app.writeJSON(r, http.StatusAccepted, returnPayload)
|
||||
}
|
||||
```
|
||||
59
docs/go/advanced/testing.md
Normal file
59
docs/go/advanced/testing.md
Normal file
@@ -0,0 +1,59 @@
|
||||
# Testing
|
||||
|
||||
Test files in go are kept alongside the source code with postfix `_test.go`. Functions beginning with `Test*` that take an argument `(t *testing.T)` will be run as unit tests. A test fails if a condition leading to `t.Error` or `t.Fail` is met.
|
||||
|
||||
```bash
|
||||
# Test everything in current directory
|
||||
go test .
|
||||
|
||||
# Test everything in subdirectories
|
||||
go test ./...
|
||||
|
||||
# See coverage percentage
|
||||
go test -cover .
|
||||
|
||||
# Generate coverage file
|
||||
go test -coverprofile=coverage.out .
|
||||
|
||||
# View code coverage from file
|
||||
go tool cover -html=coverage.out
|
||||
|
||||
# Run individual test
|
||||
go test -run Test_alpha_isPrime
|
||||
|
||||
# Run groups of tests
|
||||
go test -run Test_alpha
|
||||
```
|
||||
|
||||
## Test cases
|
||||
|
||||
Within a test function, you can run multiple related test cases by iterating over a slice of cases with `t.Run`.
|
||||
|
||||
```go
|
||||
func Test_alpha_isPrime(t *testing.T) {
|
||||
primeTests := []struct {
|
||||
name string
|
||||
num int
|
||||
expected bool
|
||||
message string
|
||||
}{
|
||||
{"prime", 7, true, "7 is prime!"},
|
||||
{"not prime", 8, false, "8 is not prime, it is divisible by 2"},
|
||||
{"zero", 0, false, "0 is not prime, by definition!"},
|
||||
{"one", 1, false, "1 is not prime, by definition!"},
|
||||
{"negative", -1, false, "Negative numbers are not prime, by definition!"},
|
||||
}
|
||||
|
||||
for _, tt := range primeTests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
actual, msg := isPrime(tt.num)
|
||||
if actual != tt.expected {
|
||||
t.Errorf("isPrime(%d): expected %t, actual %t", tt.num, tt.expected, actual)
|
||||
}
|
||||
if msg != tt.message {
|
||||
t.Errorf("isPrime(%d): expected %s, actual %s", tt.num, tt.message, msg)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -1,5 +1,9 @@
|
||||
# Collections
|
||||
|
||||
## Make
|
||||
|
||||
The `make` keyword is used to create slices, maps, and channels that are initialized and ready for use. For example, a slice is a three component object with a pointer to an array, a length, and a capacity. Until these are set, the slice is considered nil. The `make` keyword initialized these variables and makes the slice ready to use. It also returns value of type T instead of a pointer with `new`.
|
||||
|
||||
## Arrays
|
||||
|
||||
Arrays in Go are fixed length and must be declared at initialization.
|
||||
|
||||
@@ -17,3 +17,52 @@ func main() {
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Projects
|
||||
|
||||
Projects can be broken up into subpackages so they can be imported separately. The `internal` directoy can not be imported by someone using your package. Binaries should be kept in the `cmd` folder. Within `cmd` all files and any other binary files should include `package main`. In subpackages like `auth`, `token`, `hash`, and `trace` should have their respective package names.
|
||||
|
||||
```
|
||||
project-root-directory/
|
||||
go.mod
|
||||
modname.go
|
||||
modname_test.go
|
||||
auth/
|
||||
auth.go
|
||||
auth_test.go
|
||||
token/
|
||||
token.go
|
||||
token_test.go
|
||||
hash/
|
||||
hash.go
|
||||
internal/
|
||||
trace/
|
||||
trace.go
|
||||
cmd/
|
||||
prog1/
|
||||
main.go
|
||||
prog2/
|
||||
main.go
|
||||
```
|
||||
|
||||
`go.mod` includes the module name or where the code is located. For example, if the code is kept at `github.com/someuser/modname`, then the file should include:
|
||||
|
||||
```
|
||||
module github.com/someuser/modname
|
||||
```
|
||||
|
||||
In the importing code:
|
||||
|
||||
```go
|
||||
import "github.com/someuser/modname"
|
||||
import "github.com/someuser/modname/auth"
|
||||
import "github.com/someuser/modname/auth/token"
|
||||
import "github.com/someuser/modname/hash"
|
||||
```
|
||||
|
||||
To install the binaries:
|
||||
|
||||
```bash
|
||||
$ go install github.com/someuser/modname/prog1@latest
|
||||
$ go install github.com/someuser/modname/prog2@latest
|
||||
```
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Structs
|
||||
|
||||
Structs are collections of fields. They are defined as types and then created using brackets to declare the values.
|
||||
Structs are collections of fields. They are defined as types and then created using brackets to declare the values. You can also create structs with the `new` keyword which creates a pointer of type T and initializes the object with zero values.
|
||||
|
||||
```go
|
||||
import (
|
||||
|
||||
@@ -1 +1,11 @@
|
||||
# Go
|
||||
|
||||
Go is a statically-typed, compiled, and open source programming language created by Google.
|
||||
|
||||
## Creating a New Project
|
||||
|
||||
```bash
|
||||
mkdir <project_name>
|
||||
cd <project_name>
|
||||
go mod init gitlab.com/djdietrick/<project_name>
|
||||
```
|
||||
|
||||
@@ -12,5 +12,17 @@
|
||||
{ "text": "Structs", "link": "/go/basics/struct" },
|
||||
{ "text": "Concurrency", "link": "/go/basics/concurrency" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"text": "Go Advanced",
|
||||
"items": [
|
||||
{"text": "Context", "link": "/go/advanced/context"},
|
||||
{"text": "Flags", "link": "/go/advanced/flags"},
|
||||
{ "text": "Testing", "link": "/go/advanced/testing" },
|
||||
{"text": "HTTP", "link": "/go/advanced/http"},
|
||||
{"text": "RPC", "link": "/go/advanced/rpc"},
|
||||
{"text": "gRPC", "link": "/go/advanced/grpc"},
|
||||
{"text": "Databases", "link": "/go/advanced/db"}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -16,3 +16,33 @@ An array is a linear collection of data values that are accessible at numbered i
|
||||
- Removing from end: O(1)
|
||||
- Copying: O(n)
|
||||
- Traversing (including mapping, filtering, etc): O(n)
|
||||
|
||||
## Common Algorithms
|
||||
|
||||
### Binary Search
|
||||
|
||||
Binary search is useful when trying to find values in a sorted array. It works by picking a middle point to check, and then dividing the array in half based on whether the value is too big or small.
|
||||
|
||||
```python
|
||||
def binarySearch(nums, target):
|
||||
"""
|
||||
:type nums: List[int]
|
||||
:type target: int
|
||||
:rtype: int
|
||||
"""
|
||||
if len(nums) == 0:
|
||||
return -1
|
||||
|
||||
left, right = 0, len(nums) - 1
|
||||
while left <= right:
|
||||
mid = (left + right) // 2
|
||||
if nums[mid] == target:
|
||||
return mid
|
||||
elif nums[mid] < target:
|
||||
left = mid + 1
|
||||
else:
|
||||
right = mid - 1
|
||||
|
||||
# End Condition: left > right
|
||||
return -1
|
||||
```
|
||||
|
||||
@@ -31,3 +31,136 @@ The space complexity of a graph is O(v+e) where v is the number of vertices and
|
||||
|
||||
- Depth First Search Traversing: O(v+e)
|
||||
- Bredth First Search Traversing: O(v+e)
|
||||
|
||||
## Searching
|
||||
|
||||
To search a graph, you can use either DFS or BFS.
|
||||
|
||||
```python
|
||||
def DFS(graph, start):
|
||||
visited = set() # keep track of the visited nodes
|
||||
stack = [start] # use a stack to keep track of nodes to visit next
|
||||
|
||||
while stack:
|
||||
node = stack.pop() # get the next node to visit
|
||||
if node not in visited:
|
||||
visited.add(node) # mark the node as visited
|
||||
print(node, end=' ') # visit the node (print its value in this case)
|
||||
stack.extend(graph[node] - visited) # add the node's neighbors to the stack, able to remove seen nodes in python for efficiency
|
||||
|
||||
def BFS(graph, start):
|
||||
visited = set() # Keep track of the nodes that we've visited
|
||||
queue = [start] # Use a queue to implement the BFS
|
||||
|
||||
while queue:
|
||||
node = queue.pop() # Dequeue a node from front of queue
|
||||
if node not in visited:
|
||||
visited.add(node) # Mark the node as visited
|
||||
print(node, end=' ') # Visit the node (print its value in this case)
|
||||
queue.extend(graph[node]) # Enqueue all neighbours
|
||||
|
||||
graph = {
|
||||
'A': ['B', 'C'],
|
||||
'B': ['A', 'D', 'E'],
|
||||
'C': ['A', 'F'],
|
||||
'D': ['B'],
|
||||
'E': ['B', 'F'],
|
||||
'F': ['C', 'E'],
|
||||
}
|
||||
|
||||
DFS(graph, 'A') # Output: A C F E B D
|
||||
BFS(graph, 'A') # Output: A B C D E F
|
||||
```
|
||||
|
||||
## Finding Paths
|
||||
|
||||
The below algorithms find all possible paths from the start to goal. This is assuming all edges have equal weights. If you need to find the shortest path with weighted graphs, you can add the weight to the tuple stored in the stack and find the shortest after all paths have been found.
|
||||
|
||||
```python
|
||||
def dfs_paths(graph, start, goal):
|
||||
stack = [(start, [start])]
|
||||
while stack:
|
||||
(vertex, path) = stack.pop()
|
||||
# Again we are using python to subtract nodes already in the path
|
||||
# You could also check if next is not in the current path instead
|
||||
for next in graph[vertex] - set(path):
|
||||
# if next in path: continue
|
||||
if next == goal:
|
||||
yield path + [next]
|
||||
else:
|
||||
stack.append((next, path + [next]))
|
||||
|
||||
# Will always have shortest path first (if all edges are equal)
|
||||
def bfs_paths(graph, start, goal):
|
||||
queue = [(start, [start])]
|
||||
while queue:
|
||||
(vertex, path) = queue.pop(0)
|
||||
for next in graph[vertex] - set(path):
|
||||
# if next in path: continue
|
||||
if next == goal:
|
||||
yield path + [next]
|
||||
else:
|
||||
queue.append((next, path + [next]))
|
||||
```
|
||||
|
||||
### Finding Shortest Path (Djikstra's Algorithm)
|
||||
|
||||
```python
|
||||
from collections import defaultdict
|
||||
|
||||
class Graph():
|
||||
def __init__(self):
|
||||
"""
|
||||
self.edges is a dict of all possible next nodes
|
||||
e.g. {'X': ['A', 'B', 'C', 'E'], ...}
|
||||
self.weights has all the weights between two nodes,
|
||||
with the two nodes as a tuple as the key
|
||||
e.g. {('X', 'A'): 7, ('X', 'B'): 2, ...}
|
||||
"""
|
||||
self.edges = defaultdict(list)
|
||||
self.weights = {}
|
||||
|
||||
def add_edge(self, from_node, to_node, weight):
|
||||
# Note: assumes edges are bi-directional
|
||||
self.edges[from_node].append(to_node)
|
||||
self.edges[to_node].append(from_node)
|
||||
self.weights[(from_node, to_node)] = weight
|
||||
self.weights[(to_node, from_node)] = weight
|
||||
|
||||
def dijsktra(graph, initial, end):
|
||||
# shortest paths is a dict of nodes
|
||||
# whose value is a tuple of (previous node, weight)
|
||||
shortest_paths = {initial: (None, 0)}
|
||||
current_node = initial
|
||||
visited = set()
|
||||
|
||||
while current_node != end:
|
||||
visited.add(current_node)
|
||||
destinations = graph.edges[current_node]
|
||||
weight_to_current_node = shortest_paths[current_node][1]
|
||||
|
||||
for next_node in destinations:
|
||||
weight = graph.weights[(current_node, next_node)] + weight_to_current_node
|
||||
if next_node not in shortest_paths:
|
||||
shortest_paths[next_node] = (current_node, weight)
|
||||
else:
|
||||
current_shortest_weight = shortest_paths[next_node][1]
|
||||
if current_shortest_weight > weight:
|
||||
shortest_paths[next_node] = (current_node, weight)
|
||||
|
||||
next_destinations = {node: shortest_paths[node] for node in shortest_paths if node not in visited}
|
||||
if not next_destinations:
|
||||
return "Route Not Possible"
|
||||
# next node is the destination with the lowest weight
|
||||
current_node = min(next_destinations, key=lambda k: next_destinations[k][1])
|
||||
|
||||
# Work back through destinations in shortest path
|
||||
path = []
|
||||
while current_node is not None:
|
||||
path.append(current_node)
|
||||
next_node = shortest_paths[current_node][0]
|
||||
current_node = next_node
|
||||
# Reverse path
|
||||
path = path[::-1]
|
||||
return path
|
||||
```
|
||||
|
||||
18
docs/interview/ds/heap.md
Normal file
18
docs/interview/ds/heap.md
Normal file
@@ -0,0 +1,18 @@
|
||||
# Heap
|
||||
|
||||
A heap is a specialized tree-based data structure that satisfies the heap property: in a max-heap, for any given node I, the value of I is greater than or equal to the values of its children, while a min-heap has the value of I less than or equal to its children. Heaps are commonly implemented as arrays.
|
||||
|
||||
Common applications include:
|
||||
- Priority queues
|
||||
- Heap sort algorithm
|
||||
- Finding kth largest/smallest elements
|
||||
- Implementing efficient graph algorithms (Dijkstra's)
|
||||
|
||||
## Time Complexities
|
||||
|
||||
- Insert element: O(log n)
|
||||
- Delete element: O(log n)
|
||||
- Get min/max element: O(1)
|
||||
- Build heap from array: O(n)
|
||||
- Heapify (fix heap property): O(log n)
|
||||
- Search for element: O(n)
|
||||
170
docs/interview/ds/patterns.md
Normal file
170
docs/interview/ds/patterns.md
Normal file
@@ -0,0 +1,170 @@
|
||||
# Patterns
|
||||
|
||||
## Prefix Sum
|
||||
|
||||
Useful for finding the sum of elements in a subarray. In this pattern, you would store the sum of the elements up the that point in an array.
|
||||
|
||||
```python
|
||||
array = [1,2,3,4,5,6,7]
|
||||
prefix_sum = [1,3,6,10,15,21,28]
|
||||
|
||||
sum[i,j] = prefix_sum[j] - prefix_sum[i-1]
|
||||
```
|
||||
|
||||
## Two Pointers
|
||||
|
||||
Can be helpful in array problems where you iterate from the beginning and end of the array, moving the pointers towards each other until they meet. You can also start from the middle and work outwards.
|
||||
|
||||
## Sliding Window
|
||||
|
||||
Helpful for finding subarrays that meet a criteria. You can check sums of subarrays by creating a window of length k, check the currently selected elements, and then when moving the window subtract the left-most element and add the new right-most element.
|
||||
|
||||
```python
|
||||
def max_subarray(arr, k):
|
||||
n = len(arr)
|
||||
window_sum = sum(arr[:k])
|
||||
max_sum = window_sum
|
||||
max_start_index = 0
|
||||
for i in range(n-k):
|
||||
window_sum = window_sum - arr[i] + arr[i+k]
|
||||
if window_sum > max_sum:
|
||||
max_sum = window_sum
|
||||
max_start_index = i + 1
|
||||
|
||||
return arr[max_start_index:max_start_index + k], max_sum
|
||||
```
|
||||
|
||||
## Fast and Slow Pointers
|
||||
|
||||
Useful for working with arrays and linked lists. Slow pointer moves 1 node per loop, and the faster moves 2 nodes per loop. This can be used when trying to find a cycle in a linked list, or if trying to find the middle of a linked-list, since when the fast pointer is at the end, the slow pointer is in the middle.
|
||||
|
||||
## Linked List In-Place Reversal
|
||||
|
||||
You can do this by creating 3 pointers: `prev`, `curr`, and `next`.
|
||||
|
||||
```python
|
||||
def reverse_linked_list(head):
|
||||
prev = None
|
||||
curr = head
|
||||
while curr is not None:
|
||||
next = curr.next
|
||||
curr.next = prev
|
||||
prev = curr
|
||||
curr = next
|
||||
return prev
|
||||
```
|
||||
|
||||
## Monotonic Stack
|
||||
|
||||
Useful for finding the next greatest or next smaller element in an array.
|
||||
|
||||
```python
|
||||
# Find the next greater element for each item in the array
|
||||
def next_greater_element(arr):
|
||||
n = len(arr)
|
||||
stack = []
|
||||
result = [-1] * n
|
||||
|
||||
for i in range(n):
|
||||
while stack and arr[i] > arr[stack[-1]]:
|
||||
result[stack.pop()] = arr[i]
|
||||
|
||||
stack.append(i)
|
||||
|
||||
return result
|
||||
|
||||
arr = [1,4,6,3,2,7]
|
||||
result = next_greater_element(arr) # [4,6,7,7,7,-1]
|
||||
```
|
||||
|
||||
## Top K Elements
|
||||
|
||||
This pattern helps find the top K elements in a dataset that satisfy a criteria (greatest, least, etc). This can be done by using a min/max-heap, which only has K slots, where the top element is the next one to check against. Since popping and inserting in a heap is O(logn), then the time complexity of this is O(n\*logk)
|
||||
|
||||
## Overlapping Intervals
|
||||
|
||||
This pattern can be used when solving problems involving integers or ranges where they may overlap and finding intersections. This is done by sorting all intervals by the start, and then checking if the interval overlaps with the previous interval (start of next element is greater than end of previous element).
|
||||
|
||||
```python
|
||||
# Merging Intervals
|
||||
[[1,3], [2,6], [8,10], [15,18]]
|
||||
[[1,6], [8,10], [15,28]]
|
||||
|
||||
# Interval intersection
|
||||
[[0,2], [5,10], [13,23], [24,25]], [[1,5], [8,12], [15,24], [25,26]]
|
||||
[[1,2], [5,5], [8,10], [15,23], [24,24], [25,25]]
|
||||
|
||||
# Insert interval
|
||||
[[1,2], [3,5], [6,7], [8,10], [12,16]], [4,8]
|
||||
[[1,2], [3,10], [12,16]]
|
||||
```
|
||||
|
||||
## Modified Binary Search
|
||||
|
||||
This pattern is helpful when solving problems where you need to find elements in an imperfectly sorted array (nearly sorted, rotated sorted, unknown length, containing duplicates). It can also be helpful finding the first or last occurance of an element, finding the square root of a number, and finding a peak element.
|
||||
|
||||
```python
|
||||
def search_rotated_array(nums, target):
|
||||
left, right = 0, len(nums)-1
|
||||
while left <= right:
|
||||
mid = (left + right) // 2
|
||||
if nums[mid] == target:
|
||||
return mid
|
||||
if nums[left] <= nums[mid]:
|
||||
if nums[left] <= target < nums[mid]:
|
||||
right = mid - 1
|
||||
else:
|
||||
left = mid + 1
|
||||
else:
|
||||
if nums[mid] < target <= nums[right]:
|
||||
left = mid + 1
|
||||
else:
|
||||
right = mid - 1
|
||||
|
||||
return -1
|
||||
|
||||
print(search_rotated_array([4,5,6,7,0,1,2]))
|
||||
```
|
||||
|
||||
## Binary Tree Traversal
|
||||
|
||||
Traversing a tree can use either BFS or DFS, but which one you use can depend on what the use case is.
|
||||
|
||||
- To retrieve values, use DFS inorder traversal.
|
||||
- To create a copy of a tree, use DFS preorder traversal.
|
||||
- To process child nots before the parent (like deleting a tree), use DFS postorder traversal.
|
||||
- If you need to explore all nodes at current level before moving on, use BFS.
|
||||
|
||||
## Matrix Traversal
|
||||
|
||||
Matrix traversal can be seen as graph problems, so you can use DFS or BFS to solve a lot of these problems.
|
||||
|
||||
```python
|
||||
# Number of Islands problem
|
||||
def count_islands(grid):
|
||||
if not grid or not grid[0]: return 0
|
||||
|
||||
rows, cols = len(grid), len(grid[0])
|
||||
islands = 0
|
||||
|
||||
def dfs(i,j):
|
||||
if i < 0 or i >= rows or j < 0 or j >= cols or grid[i][j] != '1':
|
||||
return
|
||||
|
||||
grid[i][j] = '0'
|
||||
dfs(i+1, j)
|
||||
dfs(i-1, j)
|
||||
dfs(i, j+1)
|
||||
dfs(i, j-1)
|
||||
|
||||
for i in range(rows):
|
||||
for j in range(cols):
|
||||
if grid[i][j] == "1":
|
||||
dfs(i,j)
|
||||
islands += 1
|
||||
return islands
|
||||
```
|
||||
|
||||
## Backtracking
|
||||
|
||||
This can be used to find all potential solution paths and backtracking the paths that do not lead to a solution. This can be helpful for finding permutations, combinations, and all possible paths from start to end points.
|
||||
@@ -19,3 +19,39 @@ Each node in a binary tree has at most 2 children. Many operations on a binary
|
||||
### Binary Search Tree
|
||||
|
||||
A binary search tree or BST is a tree where the left child node is always less than the parent, and the right child node is always greater. This allows for very fast searching since it is easy to make a decision on which path to check down next.
|
||||
|
||||
## Searching
|
||||
|
||||
There are two main methods for searching trees: **Breadth First Search** and **Depth First Search**. In BFS, we traverse the tree level by level, and in DFS we traverse sub-tree by sub-tree, going down to the bottom and working our way up. BFS uses a queue (FIFO) and DFS uses a stack (LIFO). Within DFS we can traverse in order (Left-Root-Right), preorder (Root-Left-Right), or postorder (Left-Right-Root).
|
||||
|
||||
```python
|
||||
def BFS(root):
|
||||
if root is None:
|
||||
return
|
||||
|
||||
queue = [root]
|
||||
|
||||
while queue:
|
||||
node = queue.pop() # dequeue a node from the front of the queue
|
||||
print(node.value, end=' ') # visit the node (print its value in this case)
|
||||
|
||||
# Can also be done with n children if not binary tree
|
||||
# enqueue left child
|
||||
if node.left:
|
||||
queue.append(node.left)
|
||||
# enqueue right child
|
||||
if node.right:
|
||||
queue.append(node.right)
|
||||
|
||||
def DFS(node):
|
||||
if node is None:
|
||||
return
|
||||
|
||||
# This example is pre-order, but just rearrange the order in which you check to make it in-order or post-order
|
||||
# Visit the node (print its value in this case)
|
||||
print(node.value, end=' ')
|
||||
# Recursively call DFS on the left child
|
||||
DFS(node.left)
|
||||
# Recursively call DFS on the right child
|
||||
DFS(node.right)
|
||||
```
|
||||
|
||||
181
docs/interview/languages/javascript.md
Normal file
181
docs/interview/languages/javascript.md
Normal file
@@ -0,0 +1,181 @@
|
||||
# JavaScript
|
||||
|
||||
## Arrays
|
||||
|
||||
### Creating
|
||||
|
||||
```javascript
|
||||
let a = [];
|
||||
let a = [1, 2, 3];
|
||||
let a = Array(3).fill(0); // [0,0,0]
|
||||
let a = Array.from({ length: 5 }, (_, i) => i + 1); // [1,2,3,4,5]
|
||||
let a = [...Array(5).keys()]; // [0,1,2,3,4]
|
||||
```
|
||||
|
||||
### Accessing
|
||||
|
||||
```javascript
|
||||
const a = [1, 2, 3];
|
||||
console.log(a[0]); // 1
|
||||
console.log(a.at(-1)); // 3 (last element)
|
||||
```
|
||||
|
||||
### Manipulating
|
||||
|
||||
#### Adding
|
||||
```javascript
|
||||
const a = [];
|
||||
a.push(1); // Add to end
|
||||
a.unshift(2); // Add to beginning
|
||||
a.splice(1, 0, 3); // Insert 3 at index 1
|
||||
[...a, 4]; // Create new array with 4 added
|
||||
```
|
||||
|
||||
#### Removing
|
||||
```javascript
|
||||
const arr = ['bread', 'butter', 'eggs', 'milk'];
|
||||
arr.pop(); // returns and removes 'milk'
|
||||
arr.shift(); // returns and removes 'bread'
|
||||
arr.splice(1, 1); // removes 'butter'
|
||||
```
|
||||
|
||||
#### Slicing
|
||||
```javascript
|
||||
const a = ['spam', 'egg', 'bacon', 'tomato', 'ham', 'lobster'];
|
||||
a.slice(2, 5); // ['bacon', 'tomato', 'ham']
|
||||
a.slice(-3); // ['tomato', 'ham', 'lobster']
|
||||
a.slice(2); // ['bacon', 'tomato', 'ham', 'lobster']
|
||||
[...a]; // Create a copy of an array
|
||||
```
|
||||
|
||||
#### Sorting
|
||||
```javascript
|
||||
const nums = [3, 1, 3, 2, 5];
|
||||
nums.sort(); // [1, 2, 3, 3, 5]
|
||||
nums.reverse(); // [5, 3, 3, 2, 1]
|
||||
[...nums].sort((a, b) => b - a); // Sort descending, creates new array
|
||||
|
||||
const items = [
|
||||
{ name: 'john', grade: 'A', age: 15 },
|
||||
{ name: 'jane', grade: 'B', age: 12 },
|
||||
{ name: 'dave', grade: 'B', age: 10 }
|
||||
];
|
||||
items.sort((a, b) => a.age - b.age);
|
||||
```
|
||||
|
||||
#### Filtering
|
||||
```javascript
|
||||
const odds = [1, 2, 3, 4, 5].filter(x => x % 2 === 1);
|
||||
const evens = Array.from({ length: 20 }, (_, i) => i + 1).filter(x => x % 2 === 0);
|
||||
```
|
||||
|
||||
## Strings
|
||||
|
||||
Strings in JavaScript are immutable but have many built-in methods.
|
||||
|
||||
### Helper Functions
|
||||
|
||||
```javascript
|
||||
const str = 'spam';
|
||||
'I saw spamalot!'.includes(str); // true
|
||||
'I saw The Holy Grail!'.includes(str); // false
|
||||
|
||||
'12345'.repeat(5); // '1234512345123451234512345'
|
||||
|
||||
['John', 'Peter', 'Vicky'].join('#'); // 'John#Peter#Vicky'
|
||||
'Hello, world!'.endsWith('!'); // true
|
||||
'Hello, world!'.startsWith('H'); // true
|
||||
'Hello, world!'.substring(0, 5); // 'Hello'
|
||||
'Hello, world!'.slice(-6); // 'world!'
|
||||
```
|
||||
|
||||
## Objects
|
||||
|
||||
### Creating
|
||||
|
||||
```javascript
|
||||
const obj = {};
|
||||
const obj = { color: 'green', points: 5 };
|
||||
const obj = Object.create(null);
|
||||
const obj = new Object();
|
||||
```
|
||||
|
||||
### Accessing
|
||||
|
||||
```javascript
|
||||
obj.color;
|
||||
obj['color'];
|
||||
obj?.points; // Optional chaining
|
||||
const { color, points } = obj; // Destructuring
|
||||
|
||||
obj.age = 30;
|
||||
Object.assign(obj, { height: 180 }); // Merge properties
|
||||
```
|
||||
|
||||
### Removing
|
||||
|
||||
```javascript
|
||||
delete obj.age;
|
||||
```
|
||||
|
||||
### Iterating
|
||||
|
||||
```javascript
|
||||
Object.keys(obj).forEach(key => {
|
||||
// do something
|
||||
});
|
||||
|
||||
Object.values(obj).forEach(value => {
|
||||
// do something
|
||||
});
|
||||
|
||||
Object.entries(obj).forEach(([key, value]) => {
|
||||
// do something
|
||||
});
|
||||
```
|
||||
|
||||
### Object Methods
|
||||
|
||||
```javascript
|
||||
const keys = Object.keys(obj);
|
||||
const values = Object.values(obj);
|
||||
const entries = Object.entries(obj);
|
||||
const merged = Object.assign({}, obj1, obj2);
|
||||
const clone = { ...obj };
|
||||
|
||||
// Prevent modifications
|
||||
Object.freeze(obj);
|
||||
Object.seal(obj);
|
||||
```
|
||||
|
||||
## Sets and Maps
|
||||
|
||||
### Set
|
||||
|
||||
```javascript
|
||||
const set = new Set();
|
||||
set.add(1);
|
||||
set.has(1); // true
|
||||
set.delete(1);
|
||||
set.clear();
|
||||
|
||||
const unique = [...new Set([1, 2, 2, 3, 3])]; // [1, 2, 3]
|
||||
```
|
||||
|
||||
### Map
|
||||
|
||||
```javascript
|
||||
const map = new Map();
|
||||
map.set('key', 'value');
|
||||
map.get('key');
|
||||
map.has('key');
|
||||
map.delete('key');
|
||||
map.clear();
|
||||
|
||||
// Object to Map
|
||||
const map = new Map(Object.entries(obj));
|
||||
|
||||
// Map to Object
|
||||
const obj = Object.fromEntries(map);
|
||||
```
|
||||
|
||||
209
docs/interview/languages/python.md
Normal file
209
docs/interview/languages/python.md
Normal file
@@ -0,0 +1,209 @@
|
||||
# Python
|
||||
|
||||
## Arrays
|
||||
|
||||
### Creating
|
||||
|
||||
```python
|
||||
a = []
|
||||
a = [1,2,3]
|
||||
a = list(range(1,11)) # 1 ... 10
|
||||
a = [x for x in [3, 4, 5, 6, 7] if x > 5]
|
||||
a = [0] * 3 # [0,0,0]
|
||||
```
|
||||
|
||||
### Accessing
|
||||
|
||||
```python
|
||||
a = [1,2,3]
|
||||
print(a[0]) # 1
|
||||
print(a[-1]) # 3
|
||||
```
|
||||
|
||||
### Manipulating
|
||||
|
||||
#### Adding
|
||||
```python
|
||||
a = []
|
||||
a.append(1)
|
||||
a.extend([9, 11, 13])
|
||||
a += [6,7,8]
|
||||
a.insert(0, 2) # Insert 2 at index 0
|
||||
```
|
||||
|
||||
#### Removing
|
||||
```python
|
||||
li = ['bread', 'butter', 'eggs', 'milk']
|
||||
li.pop() # returns and removes 'milk'
|
||||
li.pop(2) # returns and removes 'eggs'
|
||||
del li[0] # removes 'bread'
|
||||
li.remove('butter')
|
||||
```
|
||||
|
||||
#### Slicing
|
||||
```python
|
||||
# a_list[start:end]
|
||||
# a_list[start:end:step]
|
||||
a = ['spam', 'egg', 'bacon', 'tomato', 'ham', 'lobster']
|
||||
a[2:5] # ['bacon', 'tomato', 'ham']
|
||||
a[-3:] # ['tomato', 'ham', 'lobster']
|
||||
a[-5:-2] # ['egg', 'bacon', 'tomato']
|
||||
a[:4] # ['spam', 'egg', 'bacon', 'tomato']
|
||||
a[2:] # ['bacon', 'tomato', 'ham', 'lobster']
|
||||
a[0:6:2] # ['spam', 'bacon', 'ham']
|
||||
a[6:0:-2] # ['lobster', 'tomato', 'egg']
|
||||
a[::-1] # ['lobster', 'ham', 'tomato', 'bacon', 'egg', 'spam']
|
||||
a[:] # Create a copy of an array
|
||||
```
|
||||
|
||||
#### Sorting
|
||||
```python
|
||||
li = [3, 1, 3, 2, 5]
|
||||
li.sort() # [1, 2, 3, 3, 5]
|
||||
li.reverse() # [5, 3, 3, 2, 1]
|
||||
sorted(li) # returns a sorted copy of li
|
||||
|
||||
li = [
|
||||
('john', 'A', 15),
|
||||
('jane', 'B', 12),
|
||||
('dave', 'B', 10)
|
||||
]
|
||||
sorted(li, key=lambda x: x[2])
|
||||
sorted(li, cmp=lambda x,y: 1 if x<y else -1) # Negative return means second number is "less than"
|
||||
|
||||
```
|
||||
|
||||
#### Filtering
|
||||
```python
|
||||
a = list(filter(lambda x : x % 2 == 1, range(1, 20)))
|
||||
```
|
||||
|
||||
## Strings
|
||||
|
||||
Strings act a lot like arrays/lists in Python, so many of the methods still apply.
|
||||
|
||||
### Helper Functions
|
||||
|
||||
```python
|
||||
s = 'spam'
|
||||
s in 'I saw spamalot!' # True
|
||||
s not in 'I saw The Holy Grail!' # True
|
||||
|
||||
s = '12345' * 5 # '1234512345123451234512345'
|
||||
|
||||
"#".join(["John", "Peter", "Vicky"]) # 'John#Peter#Vicky'
|
||||
"Hello, world!".endswith("!") # True
|
||||
"Hello, world!".startswith("H") # True
|
||||
```
|
||||
|
||||
## Dictionaries
|
||||
|
||||
### Creating
|
||||
|
||||
```python
|
||||
d = {}
|
||||
d = {'color': 'green', 'points', 5}
|
||||
```
|
||||
|
||||
### Accessing
|
||||
|
||||
```python
|
||||
d['color']
|
||||
d.get('points') # Returns None or value of points
|
||||
d.get('points', 0) # Default value
|
||||
|
||||
d['age'] = 30
|
||||
```
|
||||
|
||||
### Removing
|
||||
|
||||
```python
|
||||
del d['age']
|
||||
```
|
||||
|
||||
### Iterating
|
||||
|
||||
```python
|
||||
for key, value in d.items():
|
||||
# do something
|
||||
|
||||
for key in d.keys():
|
||||
# do something
|
||||
|
||||
for value in d.values():
|
||||
# do something
|
||||
```
|
||||
|
||||
### Zipping
|
||||
|
||||
```python
|
||||
group_1 = ['kai', 'abe', 'ada', 'gus', 'zoe']
|
||||
group_2 = ['jen', 'eva', 'dan', 'isa', 'meg']
|
||||
pairings = {name:name_2 for name, name_2 in zip(group_1, group_2)}
|
||||
# {'kai': 'jen', 'abe': 'eva', 'ada': 'dan', 'gus': 'isa', 'zoe': 'meg'}
|
||||
```
|
||||
|
||||
### Sorting
|
||||
|
||||
#### By Key
|
||||
```python
|
||||
my_dict = {'b': 2, 'a': 1, 'c': 3}
|
||||
sorted_dict_by_keys = sorted(my_dict.items())
|
||||
# [('a', 1), ('b', 2), ('c', 3)]
|
||||
```
|
||||
|
||||
#### By Value
|
||||
`dict.items()` returns a tuple of the key value pairs for each iteration, so we can use sorted and use the value of the pair to return a list of the sorted pairs.
|
||||
```python
|
||||
my_dict = {'b': 2, 'a': 1, 'c': 3}
|
||||
sorted_dict_by_values = sorted(my_dict.items(), key=lambda item: item[1])
|
||||
# [('a', 1), ('b', 2), ('c', 3)]
|
||||
```
|
||||
|
||||
#### OrderedDict
|
||||
OrderedDicts preserve the order in which the items were added, whereas normal dicts do not guarantee the order upon iteration. So you can sort the dictionary by key or value, and then pass that into OrderedDict to have a dict-like structure that preserves the inserted order.
|
||||
|
||||
```python
|
||||
from collections import OrderedDict
|
||||
|
||||
my_dict = {'b': 2, 'a': 4, 'c': 3}
|
||||
sorted_dict_by_keys = OrderedDict(sorted(my_dict.items()))
|
||||
for key, value in sorted_dict_by_keys:
|
||||
# do something
|
||||
```
|
||||
|
||||
## Heap
|
||||
|
||||
`heapq` is a module that implements a heap structure on a list of items. It's helpful for finding kth largest/smallest elements and priority queues.
|
||||
|
||||
```python
|
||||
import heapq
|
||||
|
||||
li = [10, 20, 15, 30, 40]
|
||||
|
||||
heapq.heapify(li)
|
||||
# [10, 20, 15, 30, 40]
|
||||
|
||||
# Appending an element
|
||||
heapq.heappush(h, 5)
|
||||
|
||||
# Pop the smallest element from the heap
|
||||
min = heapq.heappop(h)
|
||||
|
||||
# Push a new element (5), then pop the smallest element and return
|
||||
min = heapq.heappushpop(h, 5)
|
||||
|
||||
# Pop the smallest element, and then push new element (5)
|
||||
min = heapq.heapreplace(h, 5)
|
||||
|
||||
# Find the 3 largest elements
|
||||
maxi = heapq.nlargest(3, h)
|
||||
# [40, 30, 20]
|
||||
|
||||
# Find the 3 smallest elements
|
||||
min = heapq.nsmallest(3, h)
|
||||
# [10, 15, 20]
|
||||
|
||||
# Merge heaps
|
||||
h3 = list(heapq.merge(h1, h2))
|
||||
```
|
||||
@@ -17,7 +17,9 @@
|
||||
{"text": "Stacks and Queues", "link": "/interview/ds/stackqueue"},
|
||||
{"text": "Strings", "link": "/interview/ds/string"},
|
||||
{"text": "Graphs", "link": "/interview/ds/graph"},
|
||||
{"text": "Trees", "link": "/interview/ds/tree"}
|
||||
{"text": "Trees", "link": "/interview/ds/tree"},
|
||||
{"text": "Heap", "link": "/interview/ds/heap"},
|
||||
{"text": "Patterns", "link": "/interview/ds/patterns"}
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -48,5 +50,12 @@
|
||||
{"text": "Facade", "link": "/interview/patterns/structure/facade"}
|
||||
]}
|
||||
]
|
||||
},
|
||||
{
|
||||
"text": "Languages Specific",
|
||||
"items": [
|
||||
{"text": "Python", "link": "/interview/languages/python"},
|
||||
{"text": "Javascript", "link": "/interview/languages/javascript"}
|
||||
]
|
||||
}
|
||||
]
|
||||
73
docs/java/basics/classes.md
Normal file
73
docs/java/basics/classes.md
Normal file
@@ -0,0 +1,73 @@
|
||||
# Classes
|
||||
|
||||
## Generics
|
||||
|
||||
Generics allow you to create a class that can hold many different kinds of data types, and specify an upper bound for what kind of interfaces are allowed to be stored and manipulated.
|
||||
|
||||
```java
|
||||
|
||||
```
|
||||
|
||||
## Nested Classes
|
||||
|
||||
You can also have classes defined within another classes. If the nested class is static, then it can be instantiated from the class itself just like any other static methods. Otherwise, you need an instance of the class to access the nested class.
|
||||
|
||||
### Static
|
||||
|
||||
```java
|
||||
public class Employee {
|
||||
public static class EmployeeComparator <T extends Employee> implements Comparator<Employee> {
|
||||
private String sortType;
|
||||
|
||||
public EmployeeComparator(String sortType) {
|
||||
this.sortType = sortType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compare(Employee o1, Employee o2) {
|
||||
if (sortType == "yearStarted") {
|
||||
// sort by year started
|
||||
}
|
||||
// able to access private variables in Employee, and vice versa
|
||||
return o1.name.compareTo(o2.name);
|
||||
}
|
||||
}
|
||||
|
||||
private int employeeId;
|
||||
private String name;
|
||||
private int yearStarted;
|
||||
}
|
||||
|
||||
public static void main() {
|
||||
List<Employee> employees = new ArrayList<>();
|
||||
|
||||
employees.sort(new Employee.EmployeeComparator<>());
|
||||
}
|
||||
```
|
||||
|
||||
### Non-static
|
||||
|
||||
```java
|
||||
public class StoreEmployee extends Employee {
|
||||
private String store;
|
||||
|
||||
public class StoreComparator <T extends Employee> implements Comparator<StoreEmployee> {
|
||||
public int compare(StoreEmployee o1, StoreEmployee o2) {
|
||||
int result = o1.store.compareTo(o2.store);
|
||||
if (result == 0) {
|
||||
return new Employee.EmployeeComparator<>("yearStarted").compare(o1, o2);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void main() {
|
||||
List<StoreEmployee> storeEmployees = new ArrayList<>();
|
||||
|
||||
var genericEmployee = new StoreEmployee();
|
||||
var comparator = genericEmployee.new StoreComparator<>();
|
||||
var comparator = new StoreEmployee().new StoreComparator<>();
|
||||
storeEmployees.sort(comparator);
|
||||
}
|
||||
```
|
||||
51
docs/java/basics/collections.md
Normal file
51
docs/java/basics/collections.md
Normal file
@@ -0,0 +1,51 @@
|
||||
# Collections
|
||||
|
||||
Java has a class called `java.util.Collections` that defines all of the built in data types, but also provides some helper methods.
|
||||
|
||||
## Helper Methods
|
||||
|
||||
```java
|
||||
public static void main() {
|
||||
Card[] cardArray = new Card[13];
|
||||
Card aceOfHearts = Card.getFaceCard(Card.Suit.HEART, 'A');
|
||||
Arrays.fill(cardArray, aceOfHearts);
|
||||
Card.printDeck(Arrays.asList(cardArray), "Aces of Hearts", 1);
|
||||
|
||||
List<Card> cards = new ArrayList<>(52);
|
||||
// doesn't add any cards because capacity is 52 but size is 0
|
||||
Collections.fill(cards, aceOfHearts);
|
||||
System.out.println(cards);
|
||||
System.out.println("cards.size() = " + cards.size());
|
||||
|
||||
// Returns a list with n copies
|
||||
List<Card> acesOfHearts = Collections.nCopies(13, aceOfHearts);
|
||||
Card.printDeck(acesOfHearts, "Aces of Hearts", 1);
|
||||
|
||||
Card kingOfClubs = Card.getFaceCard(Card.Suit.CLUB, 'K');
|
||||
List<Card> kingsOfClubs = Collections.nCopies(13, kingOfClubs);
|
||||
Card.printDeck(kingsOfClubs, "Kings of Clubs", 1);
|
||||
|
||||
// adds elements to empty lists
|
||||
Collections.addAll(cards, cardArray);
|
||||
Collections.addAll(cards, cardArray);
|
||||
Card.printDeck(cards, "Card Collection with Aces added", 2);
|
||||
|
||||
// copyies over elements at the same index
|
||||
// in this example we have 26 cards already, so only the first 13 get overwritten
|
||||
Collections.copy(cards, kingsOfClubs);
|
||||
Card.printDeck(cards, "Card Collection with Kings copied", 2);
|
||||
|
||||
Collections.shuffle(cards);
|
||||
Collections.reverse(cards);
|
||||
|
||||
var sortingAlgorithm = Comparator.comparing(Card::rank)
|
||||
.thenComparing(Card::suit);
|
||||
Collections.sort(cards, sortingAlgorithm);
|
||||
|
||||
List<Card> subList = new ArrayList<>(cards.subList(0,4));
|
||||
int subListIndex = Collections.indexOfSubList(cards, subList);
|
||||
|
||||
// Returns true if no elements in second list are found in first list
|
||||
boolean disjoint = Collections.disjoint(cards, subList);
|
||||
}
|
||||
```
|
||||
28
docs/java/basics/controlflow.md
Normal file
28
docs/java/basics/controlflow.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# Control Flow
|
||||
|
||||
## Loops
|
||||
|
||||
### For Loop
|
||||
|
||||
```java
|
||||
for(int i = 0; i < 10; i++) {
|
||||
// do something
|
||||
}
|
||||
|
||||
List<String> myStrings = new ArrayList<>(List.of("hello", "world"));
|
||||
for (String s : myStrings) {
|
||||
// do something
|
||||
}
|
||||
for (var s : myStrings) {
|
||||
// do something
|
||||
}
|
||||
```
|
||||
|
||||
#### Interable Interfaces
|
||||
|
||||
Many collections implement the `Iterable` interface, which has a method for each.
|
||||
|
||||
```java
|
||||
List<String> words = new ArrayList<>(List.of("hello", "world"));
|
||||
words.forEach((s) -> System.out.println(s));
|
||||
```
|
||||
36
docs/java/basics/interfaces.md
Normal file
36
docs/java/basics/interfaces.md
Normal file
@@ -0,0 +1,36 @@
|
||||
# Interfaces
|
||||
|
||||
## Common Interfaces to Implement
|
||||
|
||||
### Comparator
|
||||
|
||||
Compares two instances of a class.
|
||||
|
||||
```java
|
||||
class StudentGPAComparator implements Comparator<Student> {
|
||||
@Override
|
||||
public int compare(Student o1, Student o2) {
|
||||
return (o1.gpa + o1.name).compareTo(o2.gpa + o2.name);
|
||||
}
|
||||
}
|
||||
|
||||
public static void main() {
|
||||
Comparator<Student> gpaSorter = new StudentGPAComparator();
|
||||
Arrays.sort(students, gpaSorter);
|
||||
Arrays.sort(students, gpaSorter.reversed());
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Comparable
|
||||
|
||||
Similar to Comparator, except this is implemented directly on the classes you are comparing. Without specificing a type, you will receive an `Object` to your `compareTo` function.
|
||||
|
||||
```java
|
||||
class Student implements Comparable<Student> {
|
||||
@Override
|
||||
public int compareTo(Student o) {
|
||||
return this.name.compareTo(o.name);
|
||||
}
|
||||
}
|
||||
```
|
||||
94
docs/java/basics/lambda.md
Normal file
94
docs/java/basics/lambda.md
Normal file
@@ -0,0 +1,94 @@
|
||||
# Lambdas
|
||||
|
||||
In order to pass a lambda function as an argument, it needs to be passed to a place that expects an interface that is a **functional interface**, or it only has one abstract method. This way the compiler can be sure that the function passed is the correct one for that interface.
|
||||
|
||||
The lambda function can also use variables in the calling scope, as long as the variables are final or effectively final.
|
||||
|
||||
```java
|
||||
@FunctionalInterface
|
||||
public interface Operation<T> {
|
||||
T operate(T a, T b);
|
||||
}
|
||||
|
||||
public static <T> T calculator(Operation<T> function, T v1, T v2) {
|
||||
T result = function.operate(v1, v2);
|
||||
return result;
|
||||
}
|
||||
|
||||
public static void main() {
|
||||
List<Person> people = new ArrayList<>();
|
||||
people.sort((o1, o2) -> o1.lastName.compareTo(o2.lastName));
|
||||
|
||||
int result = calculator((a,b) -> a + b, 5, 2);
|
||||
}
|
||||
|
||||
|
||||
```
|
||||
|
||||
## Common Functional Interfaces
|
||||
|
||||
These are found in the `java.util.function` package. There are four basic types of functional interfaces.
|
||||
|
||||
- Consumer -> `void accept(T t, U u?)` - Executes code without returning data (BiConsumer takes 2 args)
|
||||
- Function -> `R apply(T t, U u?)` - Return a result of an operation or function
|
||||
- Predicate -> `boolean test(T t, U u?)` - Test if a condition is true or false
|
||||
- Supplier -> `T get()` - Return an instance of something
|
||||
|
||||
### Consumer
|
||||
|
||||
A Consumer interface takes one or two arguments and performs some operation on them without returning anything.
|
||||
|
||||
```java
|
||||
public static <T> void processPoint(T t1, T t2, BiConsumer<T,T> consumer) {
|
||||
consumer.accept(t1, t2);
|
||||
}
|
||||
|
||||
public static void main() {
|
||||
BiConsumer<Double, Double> p1 = (lat, lng) -> System.out.printf("[lat:%.3f, long:%.3f]%n", lat, lng)
|
||||
processPoint(30.245, 50.343, p1);
|
||||
}
|
||||
```
|
||||
|
||||
### Predicate
|
||||
|
||||
```java
|
||||
public static void main() {
|
||||
List<String> names = new ArrayList<>(List.of("Harry", "Ron", "Hermoine"))
|
||||
names.removeIf(s -> s.startsWith("H"));
|
||||
}
|
||||
```
|
||||
|
||||
### Function
|
||||
|
||||
Similar to the Operation interface example above, this interface takes a lambda function and two values, performing some operation on the two with the `apply` method.
|
||||
|
||||
#### Operator
|
||||
|
||||
Operators have the same return and argument types, so only one type `T`. There are both `UnaryOperators` and `BiOperators`.
|
||||
|
||||
```java
|
||||
public static <T> T calculator(BinaryOperator<T> function, T v1, T v2) {
|
||||
T result = function.apply(v1, v2);
|
||||
return result;
|
||||
}
|
||||
```
|
||||
|
||||
#### Function
|
||||
|
||||
Functions can have different types for the return values and all of its arguments. There are both `Functions` and `BiFunctions`.
|
||||
|
||||
## Method References
|
||||
|
||||
Method references allow you to pass a static method instead of a lambda function, and java can infer how to pass the arguments and what the return value should be. You pass a method reference by using the class name, two colons, and then the function name.
|
||||
|
||||
```java
|
||||
public static <T> T calculator(BinaryOperator<T> function, T v1, T v2) {
|
||||
T result = function.apply(v1, v2);
|
||||
return result;
|
||||
}
|
||||
|
||||
public static void main() {
|
||||
calculator(Integer::sum, 2, 5);
|
||||
calculator(Double::sum, 7.5, 2.5);
|
||||
}
|
||||
```
|
||||
1
docs/java/basics/types.md
Normal file
1
docs/java/basics/types.md
Normal file
@@ -0,0 +1 @@
|
||||
# Types
|
||||
1
docs/java/index.md
Normal file
1
docs/java/index.md
Normal file
@@ -0,0 +1 @@
|
||||
# Java
|
||||
14
docs/java/sidebar.json
Normal file
14
docs/java/sidebar.json
Normal file
@@ -0,0 +1,14 @@
|
||||
[
|
||||
{
|
||||
"text": "Basics",
|
||||
"items": [
|
||||
{ "text": "Introduction", "link": "/java/" },
|
||||
{ "text": "Types", "link": "/java/basics/types" },
|
||||
{ "text": "Control Flow", "link": "/java/basics/controlflow" },
|
||||
{ "text": "Classes", "link": "/java/basics/classes" },
|
||||
{ "text": "Interfaces", "link": "/java/basics/interfaces" },
|
||||
{ "text": "Collections", "link": "/java/basics/collections" },
|
||||
{ "text": "Lambdas", "link": "/java/basics/lambda" }
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -5,3 +5,5 @@
|
||||
[Rust](/rust/)
|
||||
|
||||
[Go](/go/)
|
||||
|
||||
[Java](/java/)
|
||||
27
docs/python/advanced/webscraping.md
Normal file
27
docs/python/advanced/webscraping.md
Normal file
@@ -0,0 +1,27 @@
|
||||
# Webscraping
|
||||
|
||||
Python is an amazing language to use for webscraping purposes. One of the ways to do this is with the BeautifulSoup (or bs4) package.
|
||||
|
||||
```python
|
||||
import requests
|
||||
import bs4
|
||||
|
||||
result = requests.get('https://en.wikipedia.org/wiki/Jonas_Salk')
|
||||
|
||||
soup = bs4.BeautifulSoup(result.text,"lxml")
|
||||
|
||||
print(soup.select('title')[0].getText())
|
||||
# soup.select('.some_class')
|
||||
# soup.select('#some_id')
|
||||
|
||||
# Images
|
||||
pic_element = soup.select('.thumbimage')[0]
|
||||
print(pic_element['src'])
|
||||
|
||||
image_link = requests.get('http:' + pic_element['src'])
|
||||
|
||||
# Write image to file
|
||||
f = open('my_img.jpg', 'wb')
|
||||
f.write(image_link.content)
|
||||
f.close()
|
||||
```
|
||||
@@ -25,6 +25,7 @@
|
||||
{"text": "Regex", "link": "/python/advanced/regex"},
|
||||
{"text": "Testing", "link": "/python/advanced/tests"},
|
||||
{"text": "Timing", "link": "/python/advanced/timing"},
|
||||
{"text": "Webscraping", "link": "/python/advanced/webscraping"},
|
||||
{"text": "Zipping", "link": "/python/advanced/zip"}
|
||||
]
|
||||
}
|
||||
|
||||
99
docs/react/components.md
Normal file
99
docs/react/components.md
Normal file
@@ -0,0 +1,99 @@
|
||||
# Components
|
||||
|
||||
Components are the building blocks of a React application. They contained logic and templates that are reused throughout your applications. Components are essentially functions that return some JSX that can then be imported and used by parent components.
|
||||
|
||||
```tsx
|
||||
export default function Component() {
|
||||
return <h1>Some component!</h1>;
|
||||
}
|
||||
```
|
||||
|
||||
## Props
|
||||
|
||||
Props provide a way to pass data from a parent component into a child component to somehow influence their behavior. This is a one way flow, meaning changes to these props only flow from the parent to the child, but not the reverse.
|
||||
|
||||
```tsx
|
||||
// Parent
|
||||
function Parent() {
|
||||
return <Child color="red" />;
|
||||
}
|
||||
|
||||
// Child
|
||||
function Child(props) {
|
||||
return <h1>{props.color}</h1>;
|
||||
}
|
||||
// or
|
||||
function Child({ color }) {
|
||||
return <h1>{color}</h1>;
|
||||
}
|
||||
```
|
||||
|
||||
Props however can also be functions, which is how you would handle passing data from the child back to the parent.
|
||||
|
||||
```tsx
|
||||
function Parent() {
|
||||
const onSearch = (term) => {
|
||||
console.log("Searching for", term);
|
||||
};
|
||||
|
||||
return <Child onSubmit={onSearch} />;
|
||||
}
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
function Child({ onSubmit }) {
|
||||
const [term, setTerm] = useState("");
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
onSubmit(term);
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<input value={term} onChange={(e) => setTerm(e.target.value)} />
|
||||
</form>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Children Prop
|
||||
|
||||
There is a special prop called `children` which works similarly to slots in Vue. When you use a component and pass in some text or elements within the opening and closing tags, that gets passed as a prop `children` which you can use within your component.
|
||||
|
||||
```tsx
|
||||
function Child({ children }) {
|
||||
return <h1>{children}</h1>;
|
||||
}
|
||||
|
||||
function Parent() {
|
||||
return <Child>This is my children!</Child>;
|
||||
}
|
||||
```
|
||||
|
||||
### With Typescript
|
||||
|
||||
When using Typescript, you can define an interface for your props to improve intelisense in your IDE.
|
||||
|
||||
```tsx
|
||||
interface MyProps {
|
||||
color: string | null; // can be either string or null
|
||||
shade?: string; // optional prop
|
||||
}
|
||||
|
||||
function Child({ color, shade });
|
||||
```
|
||||
|
||||
## Feeding classNames to child components
|
||||
|
||||
To pass classNames to your child components, we can use a package called `classnames` to aggregate multiple classes.
|
||||
|
||||
```tsx
|
||||
import classNames from "classnames";
|
||||
|
||||
function Child({ className }) {
|
||||
const classes = classNames("myclass1 myclass2", className);
|
||||
|
||||
return <div className={classes}>My component with custom classes!</div>;
|
||||
}
|
||||
```
|
||||
78
docs/react/context.md
Normal file
78
docs/react/context.md
Normal file
@@ -0,0 +1,78 @@
|
||||
# Context
|
||||
|
||||
Context is a way of sharing state with many child components without having to pass them via props.
|
||||
|
||||
```tsx
|
||||
// context/foo.ts
|
||||
import { createContext, useState } from "react";
|
||||
|
||||
const MyContext = createContext();
|
||||
|
||||
function Provider({ children }) {
|
||||
const [value, setValue] = useState(0);
|
||||
|
||||
const incrementValue = () => {
|
||||
setValue(value + 1);
|
||||
};
|
||||
|
||||
const valueToShare = {
|
||||
value,
|
||||
incrementValue,
|
||||
};
|
||||
|
||||
return (
|
||||
<MyContext.Provider value={valueToShare}>{children}</MyContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export { Provider };
|
||||
export default MyContext;
|
||||
|
||||
// In wrapping component...
|
||||
import { Provider } from "context/foo";
|
||||
|
||||
function Parent() {
|
||||
return (
|
||||
<Provider>
|
||||
<Children />
|
||||
</Provider>
|
||||
);
|
||||
}
|
||||
|
||||
// In consuming component
|
||||
import { useContext } from "react";
|
||||
import { MyContext } from "../context/foo";
|
||||
|
||||
function Child() {
|
||||
const { value, incrementValue } = useContext(MyContext);
|
||||
}
|
||||
```
|
||||
|
||||
## useCallback
|
||||
|
||||
This hook is used to fix some bugs when calling functions stored within contexts from useEffect in a child component. Since useEffect requires the function to be added as a dependency in the second argument, it will rerun that effect whenever that function is called because calling it forces the context to rerender, which in turn creates a new version of that function. So to fix this, we need to use useCallback to create a stable reference to that function.
|
||||
|
||||
```tsx
|
||||
// context/foo.ts
|
||||
import { createContext, useState, useCallback } from "react";
|
||||
|
||||
const MyContext = createContext();
|
||||
|
||||
function Provider({ children }) {
|
||||
const [value, setValue] = useState(0);
|
||||
|
||||
const fetchValue = useCallback(() => {
|
||||
newValue = fetch("localhost:8080");
|
||||
setValue(newValue);
|
||||
}, []);
|
||||
|
||||
const valueToShare = {
|
||||
value,
|
||||
fetchValue,
|
||||
};
|
||||
|
||||
return (
|
||||
<MyContext.Provider value={valueToShare}>{children}</MyContext.Provider>
|
||||
);
|
||||
}
|
||||
```
|
||||
364
docs/react/fetching.md
Normal file
364
docs/react/fetching.md
Normal file
@@ -0,0 +1,364 @@
|
||||
# Data Fetching
|
||||
|
||||
There are a few different ways to fetch data from a server in React using a Redux store. One note is that we should never make API calls within our reducers.
|
||||
|
||||
## React Router
|
||||
|
||||
React Router is a viable option for smaller projects that do not require larger state management. This can be accomplished by using the `loader` property on a route which can be used to fetch data.
|
||||
|
||||
```ts
|
||||
// function for executing the query and returning the results
|
||||
// ex. api/queries/searchPackages.ts
|
||||
export interface PackageSummary {
|
||||
name: string;
|
||||
version: string;
|
||||
description: string;
|
||||
keywords?: string[];
|
||||
}
|
||||
|
||||
interface SearchResponse {
|
||||
objects: {
|
||||
package: {
|
||||
name: string;
|
||||
description: string;
|
||||
version: string;
|
||||
keywords: string[];
|
||||
};
|
||||
}[];
|
||||
}
|
||||
|
||||
export async function searchPackages(term: string): Promise<PackageSummary[]> {
|
||||
const res = await fetch(
|
||||
`https://registry.npmjs.org/-/v1/search?text=${term}&size=10`
|
||||
);
|
||||
const data: SearchResponse = await res.json();
|
||||
|
||||
return data.objects.map(
|
||||
({ package: { name, description, version, keywords } }) => {
|
||||
return {
|
||||
name,
|
||||
description,
|
||||
version,
|
||||
keywords,
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
```ts
|
||||
// loader function, ex. pages/search/searchLoader.ts
|
||||
import { searchPackages } from "../../api/queries/searchPackages";
|
||||
import type { PackageSummary } from "../../api/queries/packageSummary";
|
||||
|
||||
export interface SearchLoaderResult {
|
||||
searchResults: PackageSummary[];
|
||||
}
|
||||
|
||||
export async function searchLoader({
|
||||
request, // request includes the url we are currently at in our router, can be parsed for query params
|
||||
params, // named params in the route, ex. /packages/:name would be params.name
|
||||
}: {
|
||||
request: Request;
|
||||
}): Promise<SearchLoaderResult> {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const term = searchParams.get("term");
|
||||
|
||||
if (!term) {
|
||||
throw new Error("Search term must be provided");
|
||||
}
|
||||
|
||||
const results = await searchPackages(term);
|
||||
|
||||
// Good habit to return object, makes it easier to add more data in the future
|
||||
return {
|
||||
searchResults: results,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
```ts
|
||||
// router
|
||||
import { createBrowserRouter, RouterProvider } from "react-router-dom";
|
||||
import Root from "./pages/Root";
|
||||
import HomePage from "./pages/home/HomePage";
|
||||
import { homeLoader } from "./pages/home/homeLoader";
|
||||
import SearchPage from "./pages/search/SearchPage";
|
||||
import { searchLoader } from "./pages/search/searchLoader";
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
path: "/",
|
||||
element: <Root />,
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
element: <HomePage />,
|
||||
loader: homeLoader,
|
||||
},
|
||||
{
|
||||
path: "/search",
|
||||
element: <SearchPage />,
|
||||
loader: searchLoader,
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
```
|
||||
|
||||
```ts
|
||||
// in component
|
||||
import { useLoaderData } from "react-router-dom";
|
||||
import { SearchLoaderResult } from "./searchLoader";
|
||||
|
||||
export default function SearchPage() {
|
||||
const { searchResults } = useLoaderData() as SearchLoaderResult;
|
||||
|
||||
// ... use search results
|
||||
}
|
||||
```
|
||||
|
||||
## Redux Toolkit Queries
|
||||
|
||||
RTKQ is a library that allows you to create APIs to define how you want to query and manipulate your data via requests. It also automatically creates some hooks to let you know when data is loading, fetching, and functions to refetch, as well as hooks to automatically refetch data when some piece of data is changed by one of the API endpoints.
|
||||
|
||||
```ts
|
||||
// store/apis/photosApi.ts
|
||||
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";
|
||||
import { faker } from "@faker-js/faker";
|
||||
import Album from "../models/Album";
|
||||
import Photo from "../models/Photo";
|
||||
|
||||
const photosApi = createApi({
|
||||
// path in the root state variable
|
||||
reducerPath: "photos",
|
||||
baseQuery: fetchBaseQuery({
|
||||
baseUrl: "http://localhost:3005",
|
||||
}),
|
||||
tagTypes: ["Photo", "AlbumPhotos"],
|
||||
endpoints(builder) {
|
||||
return {
|
||||
fetchPhotos: builder.query({
|
||||
// Create a list of tags that will trigger a refetch if any other endpoint invalidates one of the tags
|
||||
providesTags: (result: Photo[], _, album: Album) => {
|
||||
const tags = result.map((photo) => {
|
||||
return { type: "Photo", id: photo.id };
|
||||
});
|
||||
// Is is also common to use a tag like { type: "Photo", id: "LIST/ALL/*" } to have an entry to invalidate the entire list
|
||||
// However, this will cause a refetch of all Photos lists if there are multiple
|
||||
tags.push({ type: "AlbumPhotos", id: album.id });
|
||||
return tags;
|
||||
},
|
||||
query: (album: Album) => {
|
||||
return {
|
||||
url: "/photos",
|
||||
method: "GET",
|
||||
// appended onto url
|
||||
params: {
|
||||
albumId: album.id,
|
||||
},
|
||||
};
|
||||
},
|
||||
}),
|
||||
addPhoto: builder.mutation({
|
||||
// triggers refetching for this album
|
||||
invalidatesTags: (_, __, album: Album) => {
|
||||
return [{ type: "AlbumPhotos", id: album.id }];
|
||||
},
|
||||
query: (album: Album) => ({
|
||||
url: "/photos",
|
||||
method: "POST",
|
||||
// request body parameters
|
||||
body: {
|
||||
albumId: album.id,
|
||||
url: faker.image.abstract(150, 150, true),
|
||||
},
|
||||
}),
|
||||
}),
|
||||
removePhoto: builder.mutation({
|
||||
invalidatesTags: (_, __, photo: Photo) => {
|
||||
return [{ type: "Photo", id: photo.id }];
|
||||
},
|
||||
query: (photo: Photo) => ({
|
||||
url: `/photos/${photo.id}`,
|
||||
method: "DELETE",
|
||||
}),
|
||||
}),
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
// Automatically creates functions to access your endpoints
|
||||
export const {
|
||||
useFetchPhotosQuery,
|
||||
useAddPhotoMutation,
|
||||
useRemovePhotoMutation,
|
||||
} = photosApi;
|
||||
export { photosApi };
|
||||
```
|
||||
|
||||
```ts
|
||||
// store/index.ts
|
||||
import { configureStore } from "@reduxjs/toolkit";
|
||||
import { setupListeners } from "@reduxjs/toolkit/query";
|
||||
import { photosApi } from "./apis/photosApi";
|
||||
|
||||
export const store = configureStore({
|
||||
reducer: {
|
||||
[photosApi.reducerPath]: photosApi.reducer,
|
||||
},
|
||||
middleware: (getDefaultMiddleware) => {
|
||||
return getDefaultMiddleware().concat(photosApi.middleware);
|
||||
},
|
||||
});
|
||||
|
||||
setupListeners(store.dispatch);
|
||||
|
||||
export {
|
||||
useFetchPhotosQuery,
|
||||
useAddPhotoMutation,
|
||||
useRemovePhotoMutation,
|
||||
} from "./apis/photosApi";
|
||||
```
|
||||
|
||||
```tsx
|
||||
// component
|
||||
import { useFetchPhotosQuery, useAddPhotoMutation } from "../store";
|
||||
|
||||
export default function Comp({ album }) {
|
||||
// Automatically loads data on component creation
|
||||
const { data, isLoading, error, isFetching, refetch } =
|
||||
useFetchPhotosQuery(album);
|
||||
|
||||
// Hook to call function when needed
|
||||
// results contains data, isFetching, error, etc and is updated when function is called and request processes
|
||||
const [addPhoto, results] = useAddPhotoMutation();
|
||||
}
|
||||
```
|
||||
|
||||
## Thunks
|
||||
|
||||
Thunks are a vanilla way of using Redux Toolkit to send requests and keep track of what stage those requests are in. These can be listened to in a slice's `extraReducers` to perform state updates. Each thunk has a `pending`, `fulfilled`, and `rejected` state.
|
||||
|
||||
```ts
|
||||
// store/thunks/addUser.ts
|
||||
import { createAsyncThunk } from "@reduxjs/toolkit";
|
||||
import axios from "axios";
|
||||
import { faker } from "@faker-js/faker";
|
||||
|
||||
const addUser = createAsyncThunk("users/add", async () => {
|
||||
const response = await axios.post("http://localhost:3005/users", {
|
||||
name: faker.name.fullName(),
|
||||
});
|
||||
|
||||
return response.data;
|
||||
});
|
||||
|
||||
export { addUser };
|
||||
```
|
||||
|
||||
```ts
|
||||
// store/slices/user.ts
|
||||
import { createSlice } from "@reduxjs/toolkit";
|
||||
import { fetchUsers } from "../thunks/fetchUsers";
|
||||
|
||||
const initialState: UserState = {
|
||||
data: [],
|
||||
isLoading: false,
|
||||
error: null,
|
||||
};
|
||||
|
||||
const usersSlice = createSlice({
|
||||
name: "users",
|
||||
initialState,
|
||||
reducers: {
|
||||
setUsers: (state, action) => {
|
||||
state.data = action.payload;
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
// fetch users
|
||||
builder.addCase(fetchUsers.pending, (state, _) => {
|
||||
state.isLoading = true;
|
||||
state.error = null;
|
||||
});
|
||||
builder.addCase(fetchUsers.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.data = action.payload;
|
||||
});
|
||||
builder.addCase(fetchUsers.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.error = action.error || "An error occurred.";
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const usersReducer = usersSlice.reducer;
|
||||
```
|
||||
|
||||
```ts
|
||||
// store/index.ts
|
||||
import { configureStore } from "@reduxjs/toolkit";
|
||||
import { usersReducer } from "./slices/usersSlice";
|
||||
|
||||
export const store = configureStore({
|
||||
reducer: {
|
||||
users: usersReducer,
|
||||
},
|
||||
});
|
||||
|
||||
// Export thunk function from store index
|
||||
export * from "./thunks/fetchUsers";
|
||||
export * from "./thunks/addUser";
|
||||
export * from "./thunks/deleteUser";
|
||||
```
|
||||
|
||||
### Optional Custom Hook
|
||||
|
||||
```ts
|
||||
// hooks/use-thunk.ts
|
||||
import { AsyncThunk } from "@reduxjs/toolkit";
|
||||
import { useState, useCallback } from "react";
|
||||
import { useAppDispatch } from "./store";
|
||||
|
||||
// Custom hook for handling loading and error states
|
||||
export default function useThunk(
|
||||
thunk: AsyncThunk<any, any | void, {}>
|
||||
): [(arg?: any) => void, boolean, object | null] {
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [error, setError] = useState<object | null>(null);
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const runThunk = useCallback(
|
||||
(arg?: any) => {
|
||||
setIsLoading(true);
|
||||
dispatch(thunk(arg))
|
||||
.unwrap()
|
||||
.catch((err: object) => setError(err))
|
||||
.finally(() => setIsLoading(false));
|
||||
},
|
||||
[dispatch, thunk]
|
||||
);
|
||||
|
||||
return [runThunk, isLoading, error];
|
||||
}
|
||||
```
|
||||
|
||||
```ts
|
||||
// use in component
|
||||
import useThunk from "../hooks/use-thunk";
|
||||
import { fetchUsers, addUser } from "../store";
|
||||
|
||||
export default function UsersList() {
|
||||
const [runFetchUsers, isFetching, fetchError] = useThunk(fetchUsers);
|
||||
const [runCreateUser, isCreating, createError] = useThunk(addUser);
|
||||
|
||||
useEffect(() => {
|
||||
runFetchUsers();
|
||||
}, [runFetchUsers]);
|
||||
|
||||
const handleUserAdd = () => {
|
||||
runCreateUser();
|
||||
};
|
||||
}
|
||||
```
|
||||
38
docs/react/index.md
Normal file
38
docs/react/index.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# React
|
||||
|
||||
React is a Javascript/Typescript framework for creating responsive single page applications in the web.
|
||||
|
||||
## Creating an App
|
||||
|
||||
```bash
|
||||
npm create vite@latest <app-name> -- --template=react-ts
|
||||
cd <app-name>
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### Adding Tailwindcss (Optional)
|
||||
|
||||
```bash
|
||||
npm install -D tailwindcss postcss autoprefixer
|
||||
npx tailwindcss init -p
|
||||
```
|
||||
|
||||
```js
|
||||
// tailwind.config.js
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
||||
```
|
||||
|
||||
```css
|
||||
/* index.css */
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
```
|
||||
86
docs/react/jsx.md
Normal file
86
docs/react/jsx.md
Normal file
@@ -0,0 +1,86 @@
|
||||
# JSX
|
||||
|
||||
## Rendering a List
|
||||
|
||||
```tsx
|
||||
import { Fragment } from "react";
|
||||
|
||||
function Comp() {
|
||||
const items = [
|
||||
{ id: 1, name: "Item 1" },
|
||||
{ id: 2, name: "Item 2" },
|
||||
{ id: 3, name: "Item 3" },
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
{items.map((item) => {
|
||||
return <Fragment key={item.id}>{item.name}</Fragment>;
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Conditional Rendering
|
||||
|
||||
```tsx
|
||||
function Comp() {
|
||||
const items = [
|
||||
{ id: 1, name: "Item 1", show: true },
|
||||
{ id: 2, name: "Item 2", show: true },
|
||||
{ id: 3, name: "Item 3", show: false },
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
{items.map((item) => {
|
||||
return {item.show && <Fragment key={item.id}>{item.name}</Fragment>};
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Checking if item is valid
|
||||
|
||||
```tsx
|
||||
function Comp() {
|
||||
const items: object[] | null = null;
|
||||
|
||||
return <div>{items?.length || "No items"}</div>;
|
||||
}
|
||||
```
|
||||
|
||||
## Two-way data binding
|
||||
|
||||
In order to have two-way data binding between the component and an input or another child component, you need to make use of the `onChange` event or some other event callback.
|
||||
|
||||
```tsx
|
||||
import { useState } from "react";
|
||||
|
||||
function Comp() {
|
||||
const [value, setValue] = useState("");
|
||||
|
||||
return <input value={value} onChange={(e) => setValue(e.target.value)} />;
|
||||
}
|
||||
```
|
||||
|
||||
## Handling number inputs
|
||||
|
||||
```tsx
|
||||
import { useState } from "react";
|
||||
|
||||
function Comp() {
|
||||
const [value, setValue] = useState(0);
|
||||
|
||||
const handleChange = (e) => {
|
||||
// will be NaN if invalid input or empty
|
||||
newValue = parseInt(e.target.value) || 0;
|
||||
// do something with new value
|
||||
};
|
||||
|
||||
// need || "" or else it will always have an annoying 0 in the input
|
||||
return <input type="number" value={value || ""} onChange={handleChange} />;
|
||||
}
|
||||
```
|
||||
184
docs/react/redux.md
Normal file
184
docs/react/redux.md
Normal file
@@ -0,0 +1,184 @@
|
||||
# Redux
|
||||
|
||||
Redux is a centralized state store that can be accessed from any component in your application. It acts as basically a centralized reducer, where it has a state and an dispatch method to update that state. Redux Toolkit is a library that simplifies creating actions to interact with the store, and is the recommended approach going forward.
|
||||
|
||||
```ts
|
||||
// store/slices/song.ts
|
||||
import { createSlice } from "@reduxjs/toolkit";
|
||||
import { reset } from "../actions";
|
||||
|
||||
const songsSlice = createSlice({
|
||||
name: "song",
|
||||
initialState: [],
|
||||
reducers: {
|
||||
addSong(state, action) {
|
||||
state.push(action.payload);
|
||||
},
|
||||
removeSong(state, action) {
|
||||
// action.payload === string, the song we want to remove
|
||||
const index = state.indexOf(action.payload);
|
||||
state.splice(index, 1);
|
||||
},
|
||||
},
|
||||
// listen to manual actions not created by this generator
|
||||
extraReducers(builder) {
|
||||
builder.addCase(reset, (state, action) => {
|
||||
return [];
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const { addSong, removeSong } = songsSlice.actions;
|
||||
// can also export this as the default
|
||||
export const songsReducer = songsSlice.reducer;
|
||||
```
|
||||
|
||||
```ts
|
||||
// store/actions.ts
|
||||
// Declare common actions shared by multiple reducers
|
||||
import { createAction } from "@reduxjs/toolkit";
|
||||
|
||||
export const reset = createAction("app/reset");
|
||||
```
|
||||
|
||||
```ts
|
||||
// store/index.ts
|
||||
import { configureStore } from "@reduxjs/toolkit";
|
||||
import { songsReducer, addSong, removeSong } from "./slices/songsSlice";
|
||||
import { reset } from "./actions";
|
||||
|
||||
const store = configureStore({
|
||||
reducer: {
|
||||
songs: songsReducer,
|
||||
},
|
||||
});
|
||||
|
||||
// Export store and slice actions
|
||||
export { store, reset, addSong, removeSong };
|
||||
```
|
||||
|
||||
```ts
|
||||
// index.ts
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { Provider } from "react-redux";
|
||||
import App from "./App";
|
||||
import { store } from "./store";
|
||||
|
||||
const rootElement = document.getElementById("root");
|
||||
const root = createRoot(rootElement);
|
||||
|
||||
root.render(
|
||||
<Provider store={store}>
|
||||
<App />
|
||||
</Provider>
|
||||
);
|
||||
```
|
||||
|
||||
```tsx
|
||||
// components/songs.tsx
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { createRandomSong } from "../data";
|
||||
import { addSong, removeSong } from "../store";
|
||||
|
||||
function SongPlaylist() {
|
||||
// object to call actions
|
||||
const dispatch = useDispatch();
|
||||
|
||||
// fetch state
|
||||
// can also filter/map state within here if you want
|
||||
const songPlaylist = useSelector((state) => {
|
||||
return state.songs;
|
||||
});
|
||||
|
||||
// dispatch actions with some payload
|
||||
const handleSongAdd = (song) => {
|
||||
dispatch(addSong(song));
|
||||
};
|
||||
const handleSongRemove = (song) => {
|
||||
dispatch(removeSong(song));
|
||||
};
|
||||
|
||||
const renderedSongs = songPlaylist.map((song) => {
|
||||
return (
|
||||
<li key={song}>
|
||||
{song}
|
||||
<button
|
||||
onClick={() => handleSongRemove(song)}
|
||||
className="button is-danger"
|
||||
>
|
||||
X
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="content">
|
||||
<div className="table-header">
|
||||
<h3 className="subtitle is-3">Song Playlist</h3>
|
||||
<div className="buttons">
|
||||
<button
|
||||
onClick={() => handleSongAdd(createRandomSong())}
|
||||
className="button is-link"
|
||||
>
|
||||
+ Add Song to Playlist
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<ul>{renderedSongs}</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default SongPlaylist;
|
||||
```
|
||||
|
||||
## Optimize with Typescript
|
||||
|
||||
```ts
|
||||
// store/index.ts
|
||||
export type AppStore = typeof store;
|
||||
export type RootState = ReturnType<AppStore["getState"]>;
|
||||
export type AppDispatch = AppStore["dispatch"];
|
||||
```
|
||||
|
||||
```ts
|
||||
// custom hook
|
||||
import type { TypedUseSelectorHook } from "react-redux";
|
||||
import { useDispatch, useSelector, useStore } from "react-redux";
|
||||
import type { AppDispatch, AppStore, RootState } from "./store";
|
||||
|
||||
// Use throughout your app instead of plain `useDispatch` and `useSelector`
|
||||
export const useAppDispatch = useDispatch.withTypes<AppDispatch>();
|
||||
export const useAppSelector = useSelector.withTypes<RootState>();
|
||||
export const useAppStore = useStore.withTypes<AppStore>();
|
||||
```
|
||||
|
||||
```ts
|
||||
// typing your slices
|
||||
import type { RootState } from "../";
|
||||
|
||||
interface CounterState {
|
||||
value: number;
|
||||
}
|
||||
|
||||
// Define the initial state using that type
|
||||
const initialState: CounterState = {
|
||||
value: 0,
|
||||
};
|
||||
|
||||
export const counterSlice = createSlice({
|
||||
name: "counter",
|
||||
// `createSlice` will infer the state type from the `initialState` argument
|
||||
initialState,
|
||||
reducers: {
|
||||
// Use the PayloadAction type to declare the contents of `action.payload`
|
||||
incrementByAmount: (state, action: PayloadAction<number>) => {
|
||||
state.value += action.payload;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Other code such as selectors can use the imported `RootState` type
|
||||
export const selectCount = (state: RootState) => state.counter.value;
|
||||
```
|
||||
241
docs/react/routing.md
Normal file
241
docs/react/routing.md
Normal file
@@ -0,0 +1,241 @@
|
||||
# Routing
|
||||
|
||||
## Vanilla Routing with Context
|
||||
|
||||
```tsx
|
||||
// context/navigation.ts
|
||||
import { createContext, useState, useEffect } from "react";
|
||||
|
||||
const NavigationContext = createContext();
|
||||
|
||||
function NavigationProvider({ children }) {
|
||||
const [currentPath, setCurrentPath] = useState(window.location.pathname);
|
||||
|
||||
// handle front and back arrows
|
||||
useEffect(() => {
|
||||
const handler = () => {
|
||||
setCurrentPath(window.location.pathname);
|
||||
};
|
||||
window.addEventListener("popstate", handler);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("popstate", handler);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const navigate = (to) => {
|
||||
window.history.pushState({}, "", to);
|
||||
setCurrentPath(to);
|
||||
};
|
||||
|
||||
return (
|
||||
<NavigationContext.Provider value={{ currentPath, navigate }}>
|
||||
{children}
|
||||
</NavigationContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export { NavigationProvider };
|
||||
export default NavigationContext;
|
||||
```
|
||||
|
||||
```tsx
|
||||
// Route component for conditionally rendering components
|
||||
import useNavigation from "../hooks/use-navigation";
|
||||
|
||||
function Route({ path, children }) {
|
||||
const { currentPath } = useNavigation();
|
||||
|
||||
if (path === currentPath) {
|
||||
return children;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export default Route;
|
||||
```
|
||||
|
||||
```tsx
|
||||
// example link component
|
||||
import classNames from "classnames";
|
||||
import useNavigation from "../hooks/use-navigation";
|
||||
|
||||
function Link({ to, children, className, activeClassName }) {
|
||||
const { navigate, currentPath } = useNavigation();
|
||||
|
||||
const classes = classNames(
|
||||
"text-blue-500",
|
||||
className,
|
||||
currentPath === to && activeClassName // apply styling if currently on this page
|
||||
);
|
||||
|
||||
const handleClick = (event) => {
|
||||
// handle opening new tab, use default behavior
|
||||
if (event.metaKey || event.ctrlKey) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
|
||||
navigate(to);
|
||||
};
|
||||
|
||||
return (
|
||||
<a className={classes} href={to} onClick={handleClick}>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
export default Link;
|
||||
```
|
||||
|
||||
```tsx
|
||||
// Navbar component
|
||||
import Link from "./Link";
|
||||
|
||||
function NavBar() {
|
||||
const links = [
|
||||
{ label: "Dropdown", path: "/" },
|
||||
{ label: "Accordion", path: "/accordion" },
|
||||
{ label: "Buttons", path: "/buttons" },
|
||||
];
|
||||
|
||||
const renderedLinks = links.map((link) => {
|
||||
return (
|
||||
<Link
|
||||
key={link.label}
|
||||
to={link.path}
|
||||
className="mb-3"
|
||||
activeClassName="font-bold border-l-4 border-blue-500 pl-2"
|
||||
>
|
||||
{link.label}
|
||||
</Link>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="sticky top-0 overflow-y-scroll flex flex-col items-start">
|
||||
{renderedLinks}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default NavBar;
|
||||
```
|
||||
|
||||
```tsx
|
||||
// App.ts
|
||||
import Sidebar from "./components/Sidebar";
|
||||
import Route from "./components/Route";
|
||||
import AccordionPage from "./pages/AccordionPage";
|
||||
import DropdownPage from "./pages/DropdownPage";
|
||||
import ButtonPage from "./pages/ButtonPage";
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<div className="container mx-auto grid grid-cols-6 gap-4 mt-4">
|
||||
<Sidebar />
|
||||
<div className="col-span-5">
|
||||
<Route path="/accordion">
|
||||
<AccordionPage />
|
||||
</Route>
|
||||
<Route path="/">
|
||||
<DropdownPage />
|
||||
</Route>
|
||||
<Route path="/buttons">
|
||||
<ButtonPage />
|
||||
</Route>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
```
|
||||
|
||||
## Using React Router library
|
||||
|
||||
Documentation [here](https://reactrouter.com/home).
|
||||
|
||||
```tsx
|
||||
// main.tsx
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import { createBrowserRouter, RouterProvider } from "react-router-dom";
|
||||
|
||||
import HomePage from "./pages/HomePage";
|
||||
import NotFoundPage from "./pages/NotFoundPage";
|
||||
import ProfilePage from "./pages/ProfilePage";
|
||||
import ProfilesPage from "./pages/ProfilesPage";
|
||||
|
||||
import "./index.css";
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
path: "/",
|
||||
element: <HomePage />,
|
||||
errorElement: <NotFoundPage />,
|
||||
},
|
||||
{
|
||||
path: "/profiles",
|
||||
element: <ProfilesPage />,
|
||||
children: [
|
||||
{
|
||||
path: "/profiles/:profileId",
|
||||
element: <ProfilePage />,
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
<React.StrictMode>
|
||||
<RouterProvider router={router} />
|
||||
</React.StrictMode>
|
||||
);
|
||||
```
|
||||
|
||||
```tsx
|
||||
// ProfilesPage.tsx with child component
|
||||
import { NavLink, Outlet } from "react-router-dom";
|
||||
|
||||
export default function ProfilesPage() {
|
||||
const profiles = [1, 2, 3, 4, 5];
|
||||
|
||||
return (
|
||||
<div className="flex gap-2">
|
||||
<div className="flex flex-col gap-2">
|
||||
{profiles.map((profile) => (
|
||||
// NavLink allows you to apply styling if the link is active
|
||||
<NavLink
|
||||
key={profile}
|
||||
to={`/profiles/${profile}`}
|
||||
className={({ isActive }) => {
|
||||
return isActive ? "text-primary-700" : "";
|
||||
}}
|
||||
>
|
||||
Profile {profile}
|
||||
</NavLink>
|
||||
))}
|
||||
</div>
|
||||
<!-- Outlet displays child component links if active -->
|
||||
<Outlet />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
```tsx
|
||||
// ProfilePage component using query params
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
export default function ProfilePage() {
|
||||
const params = useParams<{ profileId: string }>();
|
||||
return (
|
||||
<div>
|
||||
<h1>Profile Page {params.profileId}</h1>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
15
docs/react/sidebar.json
Normal file
15
docs/react/sidebar.json
Normal file
@@ -0,0 +1,15 @@
|
||||
[
|
||||
{
|
||||
"text": "React Basics",
|
||||
"items": [
|
||||
{ "text": "Introduction", "link": "/react/" },
|
||||
{ "text": "JSX", "link": "/react/jsx" },
|
||||
{ "text": "Components", "link": "/react/components" },
|
||||
{ "text": "State", "link": "/react/state" },
|
||||
{ "text": "Context", "link": "/react/context" },
|
||||
{ "text": "Routing", "link": "/react/routing" },
|
||||
{ "text": "Redux", "link": "/react/redux" },
|
||||
{ "text": "Data Fetching", "link": "/react/fetching" }
|
||||
]
|
||||
}
|
||||
]
|
||||
175
docs/react/state.md
Normal file
175
docs/react/state.md
Normal file
@@ -0,0 +1,175 @@
|
||||
# State and Events
|
||||
|
||||
State allows you to have the user interace with your application, update some data under the hood, and have that reflected in the page.
|
||||
|
||||
## Events
|
||||
|
||||
Events means how you handle some user actions in your application. A full list of possible events can be found [here](https://react.dev/reference/react-dom/components/common#). Essentially, you define functions that are called when certain actions are performed on elements in your page.
|
||||
|
||||
```tsx
|
||||
function App() {
|
||||
const handleClick = () => {
|
||||
console.log("button clicked");
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button onClick={handleClick}>Click me</button>;
|
||||
<button
|
||||
onClick={() => {
|
||||
console.log("Inline function click");
|
||||
}}
|
||||
>
|
||||
Click me too!
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## useState
|
||||
|
||||
State tells your application to rerender the page when something changes in your application.
|
||||
|
||||
```tsx
|
||||
import { useState } from "react";
|
||||
|
||||
function App() {
|
||||
const [count, setCount] = useState(0);
|
||||
|
||||
const handleClick = () => {
|
||||
setCount(count + 1);
|
||||
};
|
||||
|
||||
return <button onClick={handleClick} />;
|
||||
}
|
||||
```
|
||||
|
||||
## useReducer
|
||||
|
||||
A Reducer is a way of combining multiple state variables into one object. This allows you to reduce the number of rerenders if multiple pieces of state are changing at the same time. `useReducer` takes a `reducer` function and an initial state. This `reducer` function will run whenever we call `dispatch`, and the argument to `dispatch` will be passed as an argument `action` along with the current state. The `action` argument is usually an object which tells the reducer what type of update to do. Whatever is returned from this function will be the new state. The `reducer` function cannot include async/await, promises, requests, etc. It is only for updating the state. You should not update the state directly unless you are using Immer, which is a library that automatically returns the new state for you.
|
||||
|
||||
```tsx
|
||||
import produce from "immer";
|
||||
import { useReducer } from "react";
|
||||
|
||||
// Use constants for action to prevent typos
|
||||
const INCREMENT_COUNT = "increment";
|
||||
const SET_VALUE_TO_ADD = "change_value_to_add";
|
||||
const DECREMENT_COUNT = "decrement";
|
||||
const ADD_VALUE_TO_COUNT = "add_value_to_count";
|
||||
|
||||
const reducer = (state, action) => {
|
||||
switch (action.type) {
|
||||
case INCREMENT_COUNT:
|
||||
state.count = state.count + 1;
|
||||
return;
|
||||
// If not using immer, you would need to do something like...
|
||||
return {
|
||||
...state,
|
||||
count: state.count + 1,
|
||||
};
|
||||
case DECREMENT_COUNT:
|
||||
state.count = state.count - 1;
|
||||
return;
|
||||
case ADD_VALUE_TO_COUNT:
|
||||
state.count = state.count + state.valueToAdd;
|
||||
state.valueToAdd = 0;
|
||||
return;
|
||||
case SET_VALUE_TO_ADD:
|
||||
state.valueToAdd = action.payload;
|
||||
return;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
function CounterPage({ initialCount }) {
|
||||
// wrapper reducer with produce to use immer
|
||||
const [state, dispatch] = useReducer(produce(reducer), {
|
||||
count: initialCount,
|
||||
valueToAdd: 0,
|
||||
});
|
||||
|
||||
const increment = () => {
|
||||
dispatch({
|
||||
type: INCREMENT_COUNT,
|
||||
});
|
||||
};
|
||||
const decrement = () => {
|
||||
dispatch({
|
||||
type: DECREMENT_COUNT,
|
||||
});
|
||||
};
|
||||
const handleChange = (event) => {
|
||||
const value = parseInt(event.target.value) || 0;
|
||||
|
||||
dispatch({
|
||||
type: SET_VALUE_TO_ADD,
|
||||
payload: value,
|
||||
});
|
||||
};
|
||||
const handleSubmit = (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
dispatch({
|
||||
type: ADD_VALUE_TO_COUNT,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export default CounterPage;
|
||||
```
|
||||
|
||||
### Binding inputs
|
||||
|
||||
```tsx
|
||||
function Input({ onSubmit }) {
|
||||
const [text, setText] = useState("");
|
||||
|
||||
const handleChange = (event) => {
|
||||
setText(event.target.value);
|
||||
};
|
||||
|
||||
const handleSubmit = (event) => {
|
||||
event.preventDefault();
|
||||
onSubmit(text);
|
||||
setText("");
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<input className="input" value={title} onChange={handleChange} />
|
||||
<button className="button">Submit</button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## useEffect
|
||||
|
||||
useEffect is a function that runs when the application is initially rendered and sometimes when it is rerendered. The arrow function provided is always called on the initial render, and if a variable passed in the second argument is changed in the rerender, then the arrow function is called again. If no second argument is passed, then the function is called on every rerender.
|
||||
|
||||
```tsx
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
function Comp() {
|
||||
useEffect(() => {
|
||||
console.log("Run on initial render");
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
console.log("Run on every render");
|
||||
});
|
||||
|
||||
const [text, setText] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
console.log("Run when 'text' is changed");
|
||||
}, [text]);
|
||||
}
|
||||
```
|
||||
|
||||
The only thing we can return from useEffect is a function, and this function gets called before the next time the useEffect function is run, so from the second render on.
|
||||
@@ -7,6 +7,11 @@ Test functions are defined with the `#[test]` attribute. Within the tests, we u
|
||||
For unit tests, its best to write your tests in the file that it is testing but contain it with a `mod tests` with the `cfg(test)` attribute. This will exclude it from the final executable or library. Unit tests also allow you to test private functions, meaning ones that are not `pub`.
|
||||
|
||||
```rust
|
||||
#[allow(dead_code)]
|
||||
pub fn greeting(_name: &str) -> String {
|
||||
String::from("Hello!")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*; // Use this to call functions in the current file
|
||||
@@ -33,7 +38,31 @@ mod tests {
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn greater_than_100() {
|
||||
// Something illegal
|
||||
panic!("I'm supposed to panic")
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "Expected text")]
|
||||
fn expect_panic_message() {
|
||||
panic!("Expected text");
|
||||
}
|
||||
|
||||
// Ignore test
|
||||
// Can still be run on demand
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn expensive_test() {
|
||||
// code that takes an hour to run
|
||||
}
|
||||
|
||||
// Return a result to allow for the ? operator to be used to cause test failures
|
||||
#[test]
|
||||
fn result_test() -> Result<(), String> {
|
||||
if 2 + 2 == 4 {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(String::from("two plus two does not equal four"))
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -91,7 +91,7 @@ fn some_function<T, U>(t: &T, u: &U) -> i32
|
||||
|
||||
## Returning types that implement traits
|
||||
|
||||
You can only use a trait as a return value if you are returning a single type.
|
||||
You can only use a trait as a return value if you are returning a single type. For example, you can not have one path that returns a NewsArticle and one that returns a Tweet.
|
||||
|
||||
```rust
|
||||
fn returns_summarizable() -> impl Summary {
|
||||
|
||||
@@ -17,8 +17,11 @@ if num < 3 {
|
||||
|
||||
```rust
|
||||
let config_max = Some(3u8);
|
||||
let age: Result<u8, _> = "34".parse();
|
||||
if let Some(max) = config_max {
|
||||
println!("The maximum is configured to be {}", max);
|
||||
} else if let Ok(age) = age {
|
||||
println!("Age is {}", age);
|
||||
}
|
||||
```
|
||||
|
||||
@@ -113,4 +116,19 @@ fn plus_one(x: Option<i32>) -> Option<i32> {
|
||||
}
|
||||
}
|
||||
|
||||
// Or
|
||||
let x = 1;
|
||||
match x {
|
||||
1 | 2 => println!("one or two"),
|
||||
3 => println!("three"),
|
||||
_ => println!("anything"),
|
||||
}
|
||||
|
||||
// Range
|
||||
let x = 5;
|
||||
match x {
|
||||
1..=5 => println!("one through five"),
|
||||
_ => println!("something else"),
|
||||
}
|
||||
|
||||
```
|
||||
@@ -82,6 +82,21 @@ let _two = b[1];
|
||||
// let n = a[10];
|
||||
```
|
||||
|
||||
## Result
|
||||
|
||||
Results allow you to handle when something returns either a value or error. If it has a valid value, it will be of type `Ok<T>`, otherwise it will be of type `Err<E>`. See the `errors` section for more into on Results.
|
||||
|
||||
## Option
|
||||
|
||||
Options are similar to Results in that they can contain a conditional type. It can be either of type `None` or type `Some<T>`.
|
||||
|
||||
```rust
|
||||
enum Option<T> {
|
||||
None,
|
||||
Some(T),
|
||||
}
|
||||
```
|
||||
|
||||
## Type Aliases
|
||||
|
||||
```rust
|
||||
|
||||
7
docs/vue/index.md
Normal file
7
docs/vue/index.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# Vue
|
||||
|
||||
## Creating a New Project
|
||||
|
||||
```bash
|
||||
npm create vue@latest
|
||||
```
|
||||
8
docs/vue/sidebar.json
Normal file
8
docs/vue/sidebar.json
Normal file
@@ -0,0 +1,8 @@
|
||||
[
|
||||
{
|
||||
"text": "Vue Basics",
|
||||
"items": [
|
||||
{ "text": "Introduction", "link": "/vue/" }
|
||||
]
|
||||
}
|
||||
]
|
||||
Reference in New Issue
Block a user