forked from ebhomengo/niki
Merge branch 'develop' into feature/order
This commit is contained in:
commit
9776db70b5
|
|
@ -0,0 +1,74 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"git.gocasts.ir/ebhomengo/niki/repository/migrator"
|
||||
"git.gocasts.ir/ebhomengo/niki/repository/mysql"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var up bool
|
||||
var down bool
|
||||
|
||||
var migrateCmd = &cobra.Command{
|
||||
Use: "migrate",
|
||||
Short: "Run database migrations",
|
||||
Long: `This command runs the database migrations for the product service.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
migrate()
|
||||
},
|
||||
}
|
||||
|
||||
func migrate() {
|
||||
workingDir, err := os.Getwd()
|
||||
if err != nil {
|
||||
log.Fatalf("Error getting working directory: %v", err)
|
||||
}
|
||||
|
||||
migrationPath := filepath.Join(workingDir, "productapp", "repository", "migrations")
|
||||
|
||||
// to run migrations when you want to run product service locally
|
||||
if path := os.Getenv("MIGRATION_PATH"); path != "" {
|
||||
migrationPath = path
|
||||
log.Printf("Using override migration path: %s", migrationPath)
|
||||
} else {
|
||||
log.Printf("Using default migration path: %s", migrationPath)
|
||||
}
|
||||
|
||||
// TODO: Load config from environment or config file
|
||||
mgr := migrator.New(migrator.Config{
|
||||
MysqlConfig: mysql.Config{
|
||||
Username: getEnv("DB_USERNAME", "root"),
|
||||
Password: getEnv("DB_PASSWORD", ""),
|
||||
Port: 3306,
|
||||
Host: getEnv("DB_HOST", "localhost"),
|
||||
DBName: getEnv("DB_NAME", "niki_db"),
|
||||
},
|
||||
MigrationPath: migrationPath,
|
||||
MigrationDBName: "product_migrations",
|
||||
})
|
||||
|
||||
if up {
|
||||
mgr.Up()
|
||||
} else if down {
|
||||
mgr.Down()
|
||||
} else {
|
||||
log.Println("Please specify a migration direction with --up or --down")
|
||||
}
|
||||
}
|
||||
|
||||
func getEnv(key, defaultValue string) string {
|
||||
if value := os.Getenv(key); value != "" {
|
||||
return value
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
func init() {
|
||||
migrateCmd.Flags().BoolVar(&up, "up", false, "Run migrations up")
|
||||
migrateCmd.Flags().BoolVar(&down, "down", false, "Run migrations down")
|
||||
RootCmd.AddCommand(migrateCmd)
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package command
|
||||
|
||||
import "github.com/spf13/cobra"
|
||||
|
||||
var RootCmd = &cobra.Command{
|
||||
Use: "product_service",
|
||||
Short: "A CLI for Product Service",
|
||||
Long: `Product Service CLI is a tool to manage and run
|
||||
the product service, including migrations and server startup.`,
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var port string
|
||||
|
||||
var serveCmd = &cobra.Command{
|
||||
Use: "serve",
|
||||
Short: "Start the product service",
|
||||
Long: `This command starts the main product service.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
serve()
|
||||
},
|
||||
}
|
||||
|
||||
func serve() {
|
||||
log.Println("Product Service Starting...")
|
||||
|
||||
// TODO: Initialize database connection
|
||||
// TODO: Initialize service dependencies
|
||||
// TODO: Setup HTTP server with routes
|
||||
|
||||
// Setup graceful shutdown
|
||||
go func() {
|
||||
sigCh := make(chan os.Signal, 1)
|
||||
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
|
||||
<-sigCh
|
||||
log.Println("Shutting down Product Service gracefully...")
|
||||
os.Exit(0)
|
||||
}()
|
||||
|
||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprintf(w, "Product Service OK!")
|
||||
})
|
||||
|
||||
log.Printf("Product Service listening on port %s", port)
|
||||
if err := http.ListenAndServe(":"+port, nil); err != nil {
|
||||
log.Fatalf("Failed to start server: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
serveCmd.Flags().StringVarP(&port, "port", "p", "8080", "Port to run the server on")
|
||||
RootCmd.AddCommand(serveCmd)
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"git.gocasts.ir/ebhomengo/niki/cmd/productapp/command"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if err := command.RootCmd.Execute(); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,15 +1,143 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"git.gocasts.ir/ebhomengo/niki/staffapp/repository/database"
|
||||
"git.gocasts.ir/ebhomengo/niki/staffapp/service"
|
||||
)
|
||||
|
||||
func main() {
|
||||
fmt.Println(" Staffapp Server Starting...")
|
||||
|
||||
staffDb := database.New()
|
||||
staffService := service.NewStaffService(staffDb)
|
||||
|
||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprintf(w, "Staffapp OK!")
|
||||
})
|
||||
|
||||
http.HandleFunc("/staff", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
var newStaff service.Staff
|
||||
decoder := json.NewDecoder(r.Body)
|
||||
err := decoder.Decode(&newStaff)
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid request payload: "+err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
defer r.Body.Close()
|
||||
|
||||
createdStaff, err := staffService.RegisterStaff(newStaff.Name, newStaff.LastName, newStaff.PhoneNumber)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to register staff: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
json.NewEncoder(w).Encode(createdStaff)
|
||||
})
|
||||
|
||||
http.HandleFunc("/staff/", func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
idStr := r.URL.Path[len("/staff/"):]
|
||||
if idStr == "" {
|
||||
http.Error(w, "Missing staff ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
id, err := strconv.Atoi(idStr)
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid staff ID format", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
|
||||
st, err := staffService.Get(id)
|
||||
if err != nil {
|
||||
|
||||
if err.Error() == "staff not found" {
|
||||
http.Error(w, "Staff not found", http.StatusNotFound)
|
||||
} else {
|
||||
http.Error(w, "Failed to fetch staff: "+err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(st)
|
||||
|
||||
case http.MethodPut:
|
||||
|
||||
var staffData struct {
|
||||
Name string `json:"Name"`
|
||||
LastName string `json:"LastName"`
|
||||
PhoneNumber string `json:"PhoneNumber"`
|
||||
}
|
||||
decoder := json.NewDecoder(r.Body)
|
||||
if err := decoder.Decode(&staffData); err != nil {
|
||||
http.Error(w, "Invalid request payload: "+err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
defer r.Body.Close()
|
||||
|
||||
updatedStaff, err := staffService.Update(id, staffData.Name, staffData.LastName, staffData.PhoneNumber)
|
||||
if err != nil {
|
||||
|
||||
if err.Error() == "staff not found" {
|
||||
http.Error(w, "Staff not found", http.StatusNotFound)
|
||||
} else {
|
||||
http.Error(w, "Failed to update staff: "+err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(updatedStaff)
|
||||
|
||||
case http.MethodDelete:
|
||||
|
||||
err = staffService.Remove(id)
|
||||
if err != nil {
|
||||
if err.Error() == "staff not found" {
|
||||
http.Error(w, "Staff not found", http.StatusNotFound)
|
||||
} else {
|
||||
http.Error(w, "Failed to remove staff: "+err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
|
||||
default:
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
}
|
||||
})
|
||||
|
||||
http.HandleFunc("/staffs", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
list, err := staffService.List()
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to fetch staff list: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(list)
|
||||
})
|
||||
|
||||
log.Fatal(http.ListenAndServe(":8080", nil))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,8 +18,8 @@ type TestContainer struct {
|
|||
dockerPool *dockertest.Pool // the connection pool to Docker.
|
||||
mariaResource *dockertest.Resource // MariaDB Docker container resource.
|
||||
redisResource *dockertest.Resource // Redis Docker container resource.
|
||||
mariaDBConn *mysql.DB // Connection to the MariaDB database.
|
||||
redisDBConn *redisadapter.Adapter // Connection to the Redis database.
|
||||
mariaDBConn *mysql.DB // Connection to the MariaDB mysql.
|
||||
redisDBConn *redisadapter.Adapter // Connection to the Redis mysql.
|
||||
containerExpiryInSeconds uint
|
||||
}
|
||||
|
||||
|
|
@ -158,7 +158,7 @@ func (t *TestContainer) Start() {
|
|||
|
||||
return nil
|
||||
}); err != nil {
|
||||
log.Fatalf("Could not connect to database: %s", err)
|
||||
log.Fatalf("Could not connect to mysql: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
package entity
|
||||
|
||||
type Channel struct {
|
||||
ID int8
|
||||
Type NotificationType
|
||||
Provider string
|
||||
Config string
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
package entity
|
||||
|
||||
type Notification struct {
|
||||
ID int8
|
||||
Type NotificationType
|
||||
Recipinet string
|
||||
Body string
|
||||
Status NotificationStatus
|
||||
}
|
||||
|
||||
type NotificationType uint8
|
||||
|
||||
const (
|
||||
Email NotificationType = iota + 1
|
||||
SMS
|
||||
Push
|
||||
)
|
||||
|
||||
func (t NotificationType) String() string {
|
||||
switch t {
|
||||
case Email:
|
||||
return "Email"
|
||||
case SMS:
|
||||
return "SMS"
|
||||
case Push:
|
||||
return "Push"
|
||||
default:
|
||||
return "Unknown"
|
||||
}
|
||||
}
|
||||
|
||||
type NotificationStatus uint8
|
||||
|
||||
const (
|
||||
Pending NotificationStatus = iota + 1
|
||||
Success
|
||||
Failed
|
||||
)
|
||||
|
||||
func (t NotificationStatus) String() string {
|
||||
switch t {
|
||||
case Pending:
|
||||
return "Pending"
|
||||
case Success:
|
||||
return "Success"
|
||||
case Failed:
|
||||
return "Failed"
|
||||
default:
|
||||
return "Unknown"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package messagebroker
|
||||
|
||||
import "git.gocasts.ir/ebhomengo/niki/domain/notification/entity"
|
||||
|
||||
type redis struct {
|
||||
}
|
||||
|
||||
func (r *redis) AddItem(notification entity.Notification) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *redis) RemoveItem(notification entity.Notification) error {
|
||||
return nil
|
||||
}
|
||||
func (r *redis) HealthCheck() error {
|
||||
return nil
|
||||
}
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
_ "time"
|
||||
|
||||
"git.gocasts.ir/ebhomengo/niki/domain/notification/entity"
|
||||
)
|
||||
|
||||
type Notifservice struct {
|
||||
MessageBroker MessageBroker
|
||||
Repository Repository
|
||||
Channel Channel
|
||||
}
|
||||
|
||||
type MessageBroker interface {
|
||||
AddItem(notification entity.Notification) error
|
||||
RemoveItem(notification entity.Notification) error
|
||||
HealthCheck() error
|
||||
}
|
||||
|
||||
type Repository interface {
|
||||
AddItem(notification entity.Notification) error
|
||||
}
|
||||
|
||||
type Channel interface {
|
||||
SendMessage(notification entity.Notification) error
|
||||
}
|
||||
type NotificationServiceRequest struct {
|
||||
Body string
|
||||
Type entity.NotificationType
|
||||
Recipinet string
|
||||
}
|
||||
|
||||
func (n *Notifservice) NewNotification(r NotificationServiceRequest) *entity.Notification {
|
||||
// TODO add validation of notification properties
|
||||
return &entity.Notification{
|
||||
Type: r.Type,
|
||||
Recipinet: r.Recipinet,
|
||||
Body: r.Body,
|
||||
Status: entity.Pending,
|
||||
}
|
||||
}
|
||||
|
||||
func (n *Notifservice) Send(notification entity.Notification) error {
|
||||
if err := n.Channel.SendMessage(notification); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *Notifservice) Add(notification *entity.Notification) error {
|
||||
err := n.MessageBroker.AddItem(*notification)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *Notifservice) Archive(notification *entity.Notification) error {
|
||||
if err := n.MessageBroker.RemoveItem(*notification); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := n.Repository.AddItem(*notification); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package service
|
||||
|
||||
import "git.gocasts.ir/ebhomengo/niki/domain/notification/entity"
|
||||
|
||||
type sender interface {
|
||||
Send(notification entity.Notification) error
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
package donate
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
package donateapp
|
||||
|
||||
type Config struct{
|
||||
|
||||
}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
package donate_server
|
||||
|
||||
type Handler struct {
|
||||
}
|
||||
|
||||
func NewHandler() Handler {
|
||||
return Handler{}
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
-- +migrate Up
|
||||
CREATE TABLE `donates` (
|
||||
`id` bigint UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
UNIQUE INDEX `id`(`id` ASC) USING BTREE
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 84 CHARACTER SET = utf8mb3 COLLATE = utf8mb3_persian_ci ROW_FORMAT = Dynamic;
|
||||
|
||||
-- +migrate Down
|
||||
DROP TABLE IF EXISTS `donates`;
|
||||
|
|
@ -1 +0,0 @@
|
|||
package mysql
|
||||
|
|
@ -1 +0,0 @@
|
|||
package service
|
||||
|
|
@ -1 +0,0 @@
|
|||
package service
|
||||
|
|
@ -1,8 +1,10 @@
|
|||
package doanteApp
|
||||
package donate_app
|
||||
|
||||
import "net/http"
|
||||
|
||||
type Application struct {
|
||||
Config Config
|
||||
HTTPServer *http.Server
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package donate_app
|
||||
|
||||
|
||||
|
||||
|
||||
import (
|
||||
"git.gocasts.ir/ebhomengo/niki/repository/mysql"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Mysql mysql.Config `koanf:"mariadb"`
|
||||
}
|
||||
|
||||
|
||||
type Config struct{
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package donate_server
|
||||
|
||||
type Handler struct{}
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
// --- Type Aliases ---
|
||||
package pkg
|
||||
|
||||
type ID uint64
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
-- +migrate Up
|
||||
|
||||
CREATE TABLE `campaigns` (
|
||||
`id` BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
`title` VARCHAR(255) NOT NULL,
|
||||
`description` TEXT,
|
||||
`goal_amount` DECIMAL(15,2) NOT NULL,
|
||||
`raised_amount` DECIMAL(15,2) DEFAULT 0,
|
||||
`status` VARCHAR(50) NOT NULL,
|
||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
`deadline_at` TIMESTAMP,
|
||||
`admin_id` BIGINT NOT NULL,
|
||||
FOREIGN KEY (`admin_id`) REFERENCES `users`(`id`) ON DELETE RESTRICT
|
||||
);
|
||||
|
||||
|
||||
-- +migrate Down
|
||||
DROP TABLE `campaigns`;
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
-- +migrate Up
|
||||
|
||||
CREATE TABLE `campaign_participants` (
|
||||
`id` BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
`campaign_id` BIGINT NOT NULL,
|
||||
`user_id` BIGINT NOT NULL,
|
||||
`amount` DECIMAL(15,2) NOT NULL,
|
||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (`campaign_id`)
|
||||
REFERENCES `campaigns`(`id`)
|
||||
ON DELETE CASCADE,
|
||||
FOREIGN KEY (`user_id`)
|
||||
REFERENCES `users`(`id`)
|
||||
ON DELETE CASCADE,
|
||||
UNIQUE KEY `unique_campaign_user` (`campaign_id`, `user_id`)
|
||||
);
|
||||
|
||||
-- +migrate Down
|
||||
DROP TABLE `campaign_participants`;
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
package mysql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.gocasts.ir/ebhomengo/niki/campaign/entity"
|
||||
richerror "git.gocasts.ir/ebhomengo/niki/pkg/rich_error"
|
||||
"git.gocasts.ir/ebhomengo/niki/repository/mysql"
|
||||
"git.gocasts.ir/ebhomengo/niki/types"
|
||||
)
|
||||
|
||||
type DB struct {
|
||||
conn *mysql.DB
|
||||
}
|
||||
|
||||
func New(db *mysql.DB) *DB {
|
||||
return &DB{conn: db}
|
||||
}
|
||||
|
||||
// CreateCampaign creates a new campaign
|
||||
func (d *DB) CreateCampaign(ctx context.Context, campaign entity.Campaign) (types.ID, error) {
|
||||
const Op = "repository.mysql.campaign.create"
|
||||
|
||||
tx, err := d.conn.Conn().BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return 0, richerror.New(Op).WithErr(err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
query := `INSERT INTO campaigns (title, description, goal_amount, raised_amount,
|
||||
status, deadline_at ,admin_id , created_at )
|
||||
VALUES (?, ?, ?, ?, ?, ?, ? , NOW() )`
|
||||
|
||||
result, err := tx.ExecContext(ctx, query,
|
||||
campaign.Title,
|
||||
campaign.Description,
|
||||
campaign.GoalAmount,
|
||||
campaign.RaisedAmount,
|
||||
campaign.Status,
|
||||
campaign.DeadlineAt,
|
||||
campaign.AdminID,
|
||||
campaign.CreatedAt,
|
||||
|
||||
|
||||
)
|
||||
if err != nil {
|
||||
return 0, richerror.New(Op).WithErr(err)
|
||||
}
|
||||
|
||||
campaignID, err := result.LastInsertId()
|
||||
if err != nil {
|
||||
return 0, richerror.New(Op).WithErr(err)
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return 0, richerror.New(Op).WithErr(err)
|
||||
}
|
||||
|
||||
return types.ID(campaignID), nil
|
||||
}
|
||||
|
||||
// Create adds a new participant to a campaign
|
||||
func (d *DB) CreateCampaignParticipants(ctx context.Context, participant entity.CampaignParticipant) (types.ID, error) {
|
||||
const Op = "repository.mysql.campaign_participant.create"
|
||||
|
||||
query := `INSERT INTO campaign_participants (id,campaign_id, user_id, amount , created_at)
|
||||
VALUES (?, ?, ? , ? , NOW())`
|
||||
|
||||
result, err := d.conn.ExecContext(ctx, query,
|
||||
participant.ID,
|
||||
participant.CampaignID,
|
||||
participant.UserID,
|
||||
participant.Amount,
|
||||
participant.CreatedAt
|
||||
)
|
||||
if err != nil {
|
||||
return 0, richerror.New(Op).WithErr(err)
|
||||
}
|
||||
|
||||
id, err := result.LastInsertId()
|
||||
if err != nil {
|
||||
return 0, richerror.New(Op).WithErr(err)
|
||||
}
|
||||
|
||||
return types.ID(id), nil
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package service
|
||||
|
||||
|
||||
type ID uint64
|
||||
|
||||
|
||||
type CampaignParticipant struct {
|
||||
ID ID `json:"id"`
|
||||
CampaignID ID `json:"campaign_id"`
|
||||
UserID ID `json:"user_id"`
|
||||
Amount float64 `json:"amount"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// CampaignStatus represents the possible states of a campaign
|
||||
type CampaignStatus string
|
||||
|
||||
const (
|
||||
StatusActive CampaignStatus = "active"
|
||||
StatusCompleted CampaignStatus = "completed"
|
||||
StatusCancelled CampaignStatus = "cancelled"
|
||||
StatusExpired CampaignStatus = "expired" // New status for deadlines
|
||||
)
|
||||
|
||||
type ID uint64
|
||||
|
||||
type Campaign struct {
|
||||
ID ID `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
GoalAmount float64 `json:"goal_amount"`
|
||||
RaisedAmount float64 `json:"raised_amount"`
|
||||
Status CampaignStatus `json:"status"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
DeadlineAt *time.Time `json:"deadline_at,omitempty"`
|
||||
|
||||
AdminID ID `json:"creator_id"`
|
||||
}
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
package service
|
||||
|
||||
|
||||
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"git.gocasts.ir/ebhomengo/niki/campaign/entity"
|
||||
"git.gocasts.ir/ebhomengo/niki/repository"
|
||||
richerror "git.gocasts.ir/ebhomengo/niki/pkg/rich_error"
|
||||
"git.gocasts.ir/ebhomengo/niki/types"
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
||||
type CampaignService struct {
|
||||
repo repository.CampaignRepository
|
||||
participantRepo repository.CampaignParticipantRepository
|
||||
}
|
||||
|
||||
|
||||
type CampaignServiceInterface interface {
|
||||
CreateCampaign(ctx context.Context, req CampaignRepository) (types.ID, error)
|
||||
}
|
||||
|
||||
// NewCampaignService creates a new campaign service
|
||||
func NewCampaignService(
|
||||
repo repository.CampaignRepository,
|
||||
participantRepo repository.CampaignParticipantRepository,
|
||||
) *CampaignService {
|
||||
return &CampaignService{
|
||||
repo: repo,
|
||||
participantRepo: participantRepo,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// CreateCampaign creates a new campaign
|
||||
func (s *CampaignService) CreateCampaign(ctx context.Context, req CreateCampaignRequest) (types.ID, error) {
|
||||
const Op = "service.campaign.create_campaign"
|
||||
|
||||
// Business Logic: Validation
|
||||
if req.Title == "" {
|
||||
return 0, richerror.New(Op).WithErr(errors.New("title is required"))
|
||||
}
|
||||
|
||||
if req.GoalAmount <= 0 {
|
||||
return 0, richerror.New(Op).WithErr(errors.New("goal_amount must be greater than 0"))
|
||||
}
|
||||
|
||||
if req.AdminID == 0 {
|
||||
return 0, richerror.New(Op).WithErr(errors.New("admin_id is required"))
|
||||
}
|
||||
|
||||
// Business Logic: Validate status
|
||||
validStatuses := map[string]bool{
|
||||
"draft": true,
|
||||
"active": true,
|
||||
"completed": true,
|
||||
"cancelled": true,
|
||||
}
|
||||
|
||||
if !validStatuses[req.Status] {
|
||||
return 0, richerror.New(Op).WithErr(errors.New("invalid status"))
|
||||
}
|
||||
|
||||
|
||||
// Create campaign entity
|
||||
campaign := entity.Campaign{
|
||||
Title: req.Title,
|
||||
Description: req.Description,
|
||||
GoalAmount: req.GoalAmount,
|
||||
RaisedAmount: 0, // Initially 0
|
||||
Status: entity.CampaignStatus(req.Status),
|
||||
DeadlineAt: req.DeadlineAt,
|
||||
AdminID: req.AdminID,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
|
||||
// Call repository
|
||||
campaignID, err := s.repo.CreateCampaign(ctx, campaign)
|
||||
if err != nil {
|
||||
return 0, richerror.New(Op).WithErr(err)
|
||||
}
|
||||
|
||||
return campaignID, nil
|
||||
}
|
||||
14
go.mod
14
go.mod
|
|
@ -18,7 +18,7 @@ require (
|
|||
github.com/stretchr/testify v1.11.1
|
||||
github.com/swaggo/echo-swagger v1.5.2
|
||||
github.com/swaggo/swag v1.16.6
|
||||
golang.org/x/crypto v0.46.0
|
||||
golang.org/x/crypto v0.48.0
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||
)
|
||||
|
||||
|
|
@ -77,13 +77,13 @@ require (
|
|||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
|
||||
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
golang.org/x/mod v0.30.0 // indirect
|
||||
golang.org/x/net v0.48.0 // indirect
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
golang.org/x/sys v0.39.0 // indirect
|
||||
golang.org/x/text v0.32.0 // indirect
|
||||
golang.org/x/mod v0.33.0 // indirect
|
||||
golang.org/x/net v0.50.0 // indirect
|
||||
golang.org/x/sync v0.20.0 // indirect
|
||||
golang.org/x/sys v0.41.0 // indirect
|
||||
golang.org/x/text v0.35.0 // indirect
|
||||
golang.org/x/time v0.14.0 // indirect
|
||||
golang.org/x/tools v0.39.0 // indirect
|
||||
golang.org/x/tools v0.42.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
sigs.k8s.io/yaml v1.3.0 // indirect
|
||||
|
|
|
|||
28
go.sum
28
go.sum
|
|
@ -404,8 +404,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
|
|||
golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
||||
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
||||
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
|
|
@ -415,8 +415,8 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB
|
|||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
|
||||
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
|
||||
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
|
||||
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
|
|
@ -434,8 +434,8 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY
|
|||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8=
|
||||
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
|
||||
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
|
||||
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
|
||||
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
|
|
@ -448,8 +448,8 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ
|
|||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
|
|
@ -483,8 +483,8 @@ golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
|
|
@ -492,8 +492,8 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
|||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
||||
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
||||
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
|
|
@ -508,8 +508,8 @@ golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapK
|
|||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
|
||||
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
|
||||
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
|
||||
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
|
|
|||
2
main.go
2
main.go
|
|
@ -43,7 +43,7 @@ func Config() config.Config {
|
|||
}
|
||||
|
||||
func MariaDB(cfg config.Config) *mysql.DB {
|
||||
migrate := flag.Bool("migrate", false, "perform database migration")
|
||||
migrate := flag.Bool("migrate", false, "perform mysql migration")
|
||||
flag.Parse()
|
||||
if *migrate {
|
||||
migrator.New(migrator.Config{
|
||||
|
|
|
|||
|
|
@ -1 +1,37 @@
|
|||
package patientapp
|
||||
|
||||
import (
|
||||
"git.gocasts.ir/ebhomengo/niki/patientapp/config"
|
||||
"git.gocasts.ir/ebhomengo/niki/patientapp/delivery/http/analytic"
|
||||
"git.gocasts.ir/ebhomengo/niki/patientapp/repository/mysql"
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
type Application struct {
|
||||
//Config Config
|
||||
HTTPServer *config.EchoServer
|
||||
DB *mysql.DataBase
|
||||
}
|
||||
|
||||
func Setup(cfg config.Config, conn *mysql.DataBase) Application {
|
||||
|
||||
e := echo.New()
|
||||
|
||||
server := config.EchoServer{
|
||||
Router: e,
|
||||
Config: cfg,
|
||||
}
|
||||
|
||||
return Application{
|
||||
//Config: config,
|
||||
HTTPServer: &server,
|
||||
DB: conn,
|
||||
}
|
||||
}
|
||||
|
||||
func (a Application) Start() {
|
||||
|
||||
server := analytic.NewServer(a.HTTPServer)
|
||||
|
||||
_ = server.Serve()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"git.gocasts.ir/ebhomengo/niki/patientapp"
|
||||
"git.gocasts.ir/ebhomengo/niki/patientapp/config"
|
||||
"git.gocasts.ir/ebhomengo/niki/patientapp/repository/mysql"
|
||||
)
|
||||
|
||||
func main() {
|
||||
db := mysql.DataBase{}
|
||||
|
||||
cfg := config.Config{
|
||||
Port: 8080,
|
||||
Cors: config.Cors{
|
||||
AllowOrigins: []string{"*"},
|
||||
},
|
||||
ShutDownCtxTimeout: 5 * time.Second,
|
||||
}
|
||||
|
||||
app := patientapp.Setup(cfg, &db)
|
||||
|
||||
app.Start()
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Port int `koanf:"port"`
|
||||
Cors Cors `koanf:"cors"`
|
||||
ShutDownCtxTimeout time.Duration `koanf:"shutdown_context_timeout"`
|
||||
}
|
||||
|
||||
type Cors struct {
|
||||
AllowOrigins []string `koanf:"allow_origins"`
|
||||
}
|
||||
|
||||
type EchoServer struct {
|
||||
Router *echo.Echo
|
||||
Config Config
|
||||
}
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
package analytic
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
svc "git.gocasts.ir/ebhomengo/niki/patientapp/service/analytic"
|
||||
richerror "git.gocasts.ir/ebhomengo/niki/pkg/rich_error"
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
service svc.Service
|
||||
}
|
||||
|
||||
func NewHandler(service svc.Service) *Handler {
|
||||
return &Handler{
|
||||
service: service,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) Health(e echo.Context) error {
|
||||
return e.JSON(http.StatusOK, map[string]interface{}{"status": "ok"})
|
||||
}
|
||||
|
||||
func (h *Handler) PatientsAnalytic(e echo.Context) error {
|
||||
var req svc.ListPatientAnalyticRequest
|
||||
|
||||
richErr := richerror.New(richerror.Op("fetchingPatientList.PatientsAnalytic"))
|
||||
|
||||
if err := e.Bind(&req); err != nil {
|
||||
richErr = richErr.WithErr(err)
|
||||
richErr = richErr.WithKind(1)
|
||||
return echo.NewHTTPError(http.StatusBadRequest, richErr)
|
||||
}
|
||||
|
||||
response, err := h.service.List(e.Request().Context(), req)
|
||||
if err != nil {
|
||||
richErr = richErr.WithErr(err)
|
||||
richErr = richErr.WithKind(4)
|
||||
return echo.NewHTTPError(http.StatusBadRequest, richErr)
|
||||
}
|
||||
|
||||
return e.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
func (h *Handler) PatientsMapSummary(e echo.Context) error {
|
||||
richErr := richerror.New(richerror.Op("fetchingPatientMapSummary.PatientsMapSummary"))
|
||||
|
||||
var req svc.GetPatientMapSummaryRequest
|
||||
|
||||
if err := e.Bind(&req); err != nil {
|
||||
richErr = richErr.WithErr(err)
|
||||
richErr = richErr.WithKind(1)
|
||||
return echo.NewHTTPError(http.StatusBadRequest, richErr)
|
||||
}
|
||||
|
||||
resp, svcErr := h.service.GetMapSummary(e.Request().Context(), req)
|
||||
if svcErr != nil {
|
||||
richErr = richErr.WithErr(svcErr)
|
||||
richErr = richErr.WithKind(4)
|
||||
return echo.NewHTTPError(http.StatusBadRequest, richErr)
|
||||
}
|
||||
|
||||
return e.JSON(http.StatusOK, resp)
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package analytic
|
||||
|
||||
import (
|
||||
"git.gocasts.ir/ebhomengo/niki/patientapp/repository/mysql"
|
||||
analytic2 "git.gocasts.ir/ebhomengo/niki/patientapp/service/analytic"
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
func NewPatientAnalyticRouter(s *echo.Group) {
|
||||
|
||||
mysqlRepo := mysql.NewPatientRepo()
|
||||
//rpcRepo := grpc.NewPatientRepo()
|
||||
|
||||
analyticService := analytic2.NewPatientAnalyticService(mysqlRepo)
|
||||
|
||||
h := NewHandler(analyticService)
|
||||
|
||||
s.GET("/patients", h.PatientsAnalytic)
|
||||
s.GET("/patients-summary", h.PatientsMapSummary)
|
||||
s.GET("/health", h.Health)
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
package analytic
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"git.gocasts.ir/ebhomengo/niki/patientapp/config"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
HTTPServer *config.EchoServer
|
||||
}
|
||||
|
||||
func NewServer(server *config.EchoServer) *Server {
|
||||
|
||||
return &Server{
|
||||
HTTPServer: server,
|
||||
}
|
||||
}
|
||||
|
||||
func (s Server) Serve() error {
|
||||
s.RegisterRoutes()
|
||||
// Start server
|
||||
return s.HTTPServer.Router.Start(fmt.Sprintf(":%d", s.HTTPServer.Config.Port))
|
||||
}
|
||||
|
||||
func (s Server) Stop(ctx context.Context) error {
|
||||
return s.HTTPServer.Router.Shutdown(ctx)
|
||||
|
||||
}
|
||||
|
||||
func (s Server) RegisterRoutes() {
|
||||
|
||||
v1 := s.HTTPServer.Router.Group("/v1")
|
||||
{
|
||||
// Analytic Group
|
||||
analyticGroup := v1.Group("/analytic")
|
||||
NewPatientAnalyticRouter(analyticGroup)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
package grpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.gocasts.ir/ebhomengo/niki/patientapp/service/analytic"
|
||||
"git.gocasts.ir/ebhomengo/niki/patientapp/service/entity"
|
||||
)
|
||||
|
||||
type AnalyticRepository struct{}
|
||||
|
||||
func NewPatientRepo() *AnalyticRepository {
|
||||
|
||||
return &AnalyticRepository{}
|
||||
}
|
||||
|
||||
func (db *AnalyticRepository) GetPatients(ctx context.Context, f analytic.PatientFilter) ([]entity.Patient, error) {
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (db *AnalyticRepository) CountPatients(ctx context.Context, f analytic.PatientFilter) (int, error) {
|
||||
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func (db *AnalyticRepository) SummaryByCity(ctx context.Context, provinceID uint, f analytic.PatientMapFilter) (map[uint][]entity.MapSummaryItem, error) {
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (db *AnalyticRepository) SummaryByProvince(ctx context.Context, f analytic.PatientMapFilter) (map[uint][]entity.MapSummaryItem, error) {
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
package mysql
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.gocasts.ir/ebhomengo/niki/patientapp/service/analytic"
|
||||
"git.gocasts.ir/ebhomengo/niki/patientapp/service/entity"
|
||||
)
|
||||
|
||||
type DataBase struct{}
|
||||
|
||||
func NewPatientRepo() *DataBase {
|
||||
|
||||
return &DataBase{}
|
||||
}
|
||||
|
||||
func (db *DataBase) GetPatients(ctx context.Context, f analytic.PatientFilter) ([]entity.Patient, error) {
|
||||
|
||||
return nil, nil
|
||||
|
||||
}
|
||||
|
||||
func (db *DataBase) CountPatients(ctx context.Context, f analytic.PatientFilter) (int, error) {
|
||||
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func (db *DataBase) SummaryByCity(ctx context.Context, provinceID uint, f analytic.PatientMapFilter) (map[uint][]entity.MapSummaryItem, error) {
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (db *DataBase) SummaryByProvince(ctx context.Context, f analytic.PatientMapFilter) (map[uint][]entity.MapSummaryItem, error) {
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
package analytic
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/jalaali/go-jalaali"
|
||||
"time"
|
||||
)
|
||||
|
||||
func normalizeLimitOffset(limit, offset int) (int, int) {
|
||||
if limit <= 0 {
|
||||
limit = 50
|
||||
}
|
||||
if limit > 100 {
|
||||
limit = 100
|
||||
}
|
||||
if offset < 0 {
|
||||
offset = 0
|
||||
}
|
||||
return limit, offset
|
||||
}
|
||||
|
||||
// convert age range -> DOB range
|
||||
func ageRangeToDOB(minAge, maxAge *int, now time.Time) (dobFrom, dobTo *string) {
|
||||
if maxAge != nil {
|
||||
t := now.AddDate(-(*maxAge + 1), 0, 1)
|
||||
jy, jm, jd, err := jalaali.ToJalaali(t.Year(), t.Month(), t.Day())
|
||||
if err != nil {
|
||||
}
|
||||
s := fmt.Sprintf("%04d/%02d/%02d", jy, jm, jd)
|
||||
dobFrom = &s
|
||||
}
|
||||
|
||||
if minAge != nil {
|
||||
t := now.AddDate(-*minAge, 0, 0)
|
||||
jy, jm, jd, err := jalaali.ToJalaali(t.Year(), t.Month(), t.Day())
|
||||
if err != nil {
|
||||
}
|
||||
s := fmt.Sprintf("%04d/%02d/%02d", jy, jm, jd)
|
||||
dobTo = &s
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
package analytic
|
||||
|
||||
import (
|
||||
"git.gocasts.ir/ebhomengo/niki/patientapp/service/entity"
|
||||
)
|
||||
|
||||
type ListPatientAnalyticRequest struct {
|
||||
// All fields are optional
|
||||
MinAge *int `query:"minAge,omitempty"`
|
||||
MaxAge *int `query:"maxAge,omitempty"`
|
||||
Sex *entity.Sex `query:"sex,omitempty"`
|
||||
|
||||
City *int64 `query:"city,omitempty"`
|
||||
Province *int64 `query:"province,omitempty"`
|
||||
|
||||
Search *string `query:"search,omitempty"`
|
||||
|
||||
Pagination *Pagination `query:"pagination,omitempty"`
|
||||
}
|
||||
|
||||
type PatientAnalyticItem struct {
|
||||
ID int64 `json:"id"`
|
||||
FirstName string `json:"first_name"`
|
||||
LastName string `json:"Last_name"`
|
||||
DateOfBirth string `json:"dob,omitempty"`
|
||||
Sex entity.Sex `json:"sex"`
|
||||
Phone string `json:"phone"`
|
||||
Address entity.Address `json:"address"`
|
||||
}
|
||||
type PatientAnalyticResponse struct {
|
||||
Items []PatientAnalyticItem `json:"items"`
|
||||
Pagination *Pagination `json:"pagination"`
|
||||
Total int `json:"total"`
|
||||
}
|
||||
|
||||
func ToPatientResponse(patient entity.Patient) PatientAnalyticItem {
|
||||
return PatientAnalyticItem{
|
||||
ID: patient.ID,
|
||||
FirstName: patient.FirstName,
|
||||
LastName: patient.LastName,
|
||||
DateOfBirth: patient.DateOfBirth,
|
||||
Sex: patient.Sex,
|
||||
Phone: patient.Phone,
|
||||
Address: entity.Address{
|
||||
ProvinceID: patient.Address.ProvinceID,
|
||||
CityID: patient.Address.CityID,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// GetPatientMapSummaryRequest =========================== Map ==================================
|
||||
type GetPatientMapSummaryRequest struct {
|
||||
Level entity.MapLevel `query:"level"`
|
||||
ParentID *int `query:"parentID"`
|
||||
|
||||
MinAge *int `query:"minAge,omitempty"`
|
||||
MaxAge *int `query:"maxAge,omitempty"`
|
||||
Sex *entity.Sex `query:"sex,omitempty"`
|
||||
Search *string `query:"search,omitempty"`
|
||||
}
|
||||
|
||||
type GetPatientMapSummaryResponse struct {
|
||||
Level entity.MapLevel `json:"level"`
|
||||
Items map[uint][]entity.MapSummaryItem `json:"items"`
|
||||
}
|
||||
|
||||
// Pagination ================================ Pagination =============================
|
||||
type Pagination struct {
|
||||
Limit int `query:"limit,omitempty"`
|
||||
Offset int `query:"offset,omitempty"`
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package analytic
|
||||
|
||||
import (
|
||||
"git.gocasts.ir/ebhomengo/niki/patientapp/service/entity"
|
||||
)
|
||||
|
||||
type PatientFilter struct {
|
||||
DOBFrom *string // born after
|
||||
DOBTo *string // born before
|
||||
Sex *entity.Sex
|
||||
|
||||
City *int64
|
||||
Province *int64
|
||||
Country *int64
|
||||
|
||||
Search *string
|
||||
|
||||
Limit int
|
||||
Offset int
|
||||
}
|
||||
|
||||
type PatientMapFilter struct {
|
||||
MinDOB *string
|
||||
MaxDOB *string
|
||||
Sex *entity.Sex
|
||||
Search *string
|
||||
}
|
||||
|
|
@ -0,0 +1,119 @@
|
|||
package analytic
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"git.gocasts.ir/ebhomengo/niki/patientapp/service/entity"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInvalidProvinceID = errors.New("invalid province id")
|
||||
ErrInvalidCountryID = errors.New("invalid country id")
|
||||
ErrInvalidMapLevel = errors.New("invalid map level")
|
||||
)
|
||||
|
||||
type Repository interface {
|
||||
GetPatients(ctx context.Context, f PatientFilter) ([]entity.Patient, error)
|
||||
CountPatients(ctx context.Context, f PatientFilter) (int, error)
|
||||
|
||||
SummaryByCity(ctx context.Context, provinceID uint, f PatientMapFilter) (map[uint][]entity.MapSummaryItem, error)
|
||||
SummaryByProvince(ctx context.Context, f PatientMapFilter) (map[uint][]entity.MapSummaryItem, error)
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
repository Repository
|
||||
}
|
||||
|
||||
func NewPatientAnalyticService(repo Repository) Service {
|
||||
return Service{
|
||||
repository: repo,
|
||||
}
|
||||
}
|
||||
|
||||
func (s Service) List(ctx context.Context, req ListPatientAnalyticRequest) (PatientAnalyticResponse, error) {
|
||||
|
||||
limit, offset := normalizeLimitOffset(req.Pagination.Limit, req.Pagination.Offset)
|
||||
|
||||
// convert age range
|
||||
dobFrom, dobTo := ageRangeToDOB(req.MinAge, req.MaxAge, time.Now())
|
||||
|
||||
filter := PatientFilter{
|
||||
DOBFrom: dobFrom,
|
||||
DOBTo: dobTo,
|
||||
Sex: req.Sex,
|
||||
City: req.City,
|
||||
Province: req.Province,
|
||||
Search: req.Search,
|
||||
Limit: limit,
|
||||
Offset: offset,
|
||||
}
|
||||
|
||||
items, err := s.repository.GetPatients(ctx, filter)
|
||||
if err != nil {
|
||||
return PatientAnalyticResponse{}, fmt.Errorf("GetPatients: %w", err)
|
||||
}
|
||||
|
||||
total, err := s.repository.CountPatients(ctx, filter)
|
||||
if err != nil {
|
||||
return PatientAnalyticResponse{}, fmt.Errorf("CountPatients: %w", err)
|
||||
}
|
||||
|
||||
// mapping response
|
||||
out := make([]PatientAnalyticItem, 0, len(items))
|
||||
for _, value := range items {
|
||||
out = append(out, ToPatientResponse(value))
|
||||
}
|
||||
|
||||
return PatientAnalyticResponse{
|
||||
Items: out,
|
||||
Pagination: &Pagination{
|
||||
Limit: limit,
|
||||
Offset: offset,
|
||||
},
|
||||
Total: total,
|
||||
}, nil
|
||||
|
||||
}
|
||||
|
||||
func (s Service) GetMapSummary(ctx context.Context, req GetPatientMapSummaryRequest) (GetPatientMapSummaryResponse, error) {
|
||||
|
||||
dobFrom, dobTo := ageRangeToDOB(req.MinAge, req.MaxAge, time.Now())
|
||||
|
||||
filter := PatientMapFilter{
|
||||
MinDOB: dobFrom,
|
||||
MaxDOB: dobTo,
|
||||
Sex: req.Sex,
|
||||
Search: req.Search,
|
||||
}
|
||||
|
||||
switch req.Level {
|
||||
case entity.MapLevelCity:
|
||||
if req.ParentID == nil || *req.ParentID <= 0 {
|
||||
return GetPatientMapSummaryResponse{}, ErrInvalidProvinceID
|
||||
}
|
||||
|
||||
items, err := s.repository.SummaryByCity(ctx, uint(*req.ParentID), filter)
|
||||
if err != nil {
|
||||
return GetPatientMapSummaryResponse{}, fmt.Errorf("SummaryByCity: %w", err)
|
||||
}
|
||||
return GetPatientMapSummaryResponse{Level: req.Level, Items: items}, nil
|
||||
|
||||
case entity.MapLevelProvince:
|
||||
if req.ParentID == nil || *req.ParentID <= 0 {
|
||||
return GetPatientMapSummaryResponse{}, ErrInvalidCountryID
|
||||
}
|
||||
|
||||
items, err := s.repository.SummaryByProvince(ctx, filter)
|
||||
if err != nil {
|
||||
return GetPatientMapSummaryResponse{}, fmt.Errorf("SummaryByProvince: %w", err)
|
||||
}
|
||||
return GetPatientMapSummaryResponse{Level: req.Level, Items: items}, nil
|
||||
|
||||
default:
|
||||
return GetPatientMapSummaryResponse{}, ErrInvalidMapLevel
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
package entity
|
||||
|
||||
type Address struct {
|
||||
ID uint
|
||||
PostalCode string
|
||||
Address string
|
||||
Name string
|
||||
Lat float64
|
||||
Lon float64
|
||||
CityID uint
|
||||
ProvinceID uint
|
||||
}
|
||||
|
||||
type AddressAggregated struct {
|
||||
Address Address
|
||||
Province Province
|
||||
City City
|
||||
}
|
||||
|
||||
type Province struct {
|
||||
ID uint
|
||||
Name string
|
||||
}
|
||||
type City struct {
|
||||
ID uint
|
||||
Name string
|
||||
ProvinceID uint
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package entity
|
||||
|
||||
type MapLevel string
|
||||
|
||||
const (
|
||||
MapLevelCity MapLevel = "city"
|
||||
MapLevelProvince MapLevel = "province"
|
||||
MapLevelCountry MapLevel = "country"
|
||||
)
|
||||
|
||||
type MapSummaryItem struct {
|
||||
LocationID int64
|
||||
Name string
|
||||
Count int
|
||||
CentroidLat float64
|
||||
CentroidLng float64
|
||||
}
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
package entity
|
||||
|
||||
type Patient struct {
|
||||
ID int64
|
||||
FirstName string
|
||||
LastName string
|
||||
DateOfBirth string
|
||||
Sex Sex
|
||||
Phone string
|
||||
Address Address
|
||||
CaseStatus CaseStatus
|
||||
ReferralSource ReferralSource
|
||||
AssignedStaffId int64
|
||||
StartDate string
|
||||
EndDate string
|
||||
}
|
||||
|
||||
// Sex ================================== Sex type ==========================================
|
||||
type Sex string
|
||||
|
||||
const (
|
||||
SexUnknown Sex = "unknown"
|
||||
SexMale Sex = "male"
|
||||
SexFemale Sex = "female"
|
||||
SexOther Sex = "other"
|
||||
)
|
||||
|
||||
func (s Sex) SexValidation() bool {
|
||||
switch s {
|
||||
case SexUnknown, SexMale, SexFemale, SexOther:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// CaseStatus =================================== Case Status =======================================
|
||||
type CaseStatus string
|
||||
|
||||
const (
|
||||
Open CaseStatus = "open"
|
||||
Close CaseStatus = "close"
|
||||
InProgress CaseStatus = "inProgress"
|
||||
)
|
||||
|
||||
func (s CaseStatus) CaseStatusValidation() bool {
|
||||
switch s {
|
||||
case Open, Close, InProgress:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// ReferralSource =================================== Referral source =======================================
|
||||
type ReferralSource string
|
||||
|
||||
const (
|
||||
Hospital ReferralSource = "hospital"
|
||||
Community ReferralSource = "community"
|
||||
Other ReferralSource = "other"
|
||||
)
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package date_parser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ParseDate parses a date string in "YYYY-MM-DD" format and returns a time.Time object
|
||||
func ParseDate(input string) (time.Time, error) {
|
||||
const layout = "2006-01-02"
|
||||
|
||||
// Parse the input string
|
||||
convertedDate, err := time.Parse(layout, input)
|
||||
if err != nil {
|
||||
return time.Time{}, fmt.Errorf("invalid date format: %v", err)
|
||||
}
|
||||
|
||||
return convertedDate, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
-- +migrate Up
|
||||
CREATE TABLE `categories` (
|
||||
`id` INT PRIMARY KEY AUTO_INCREMENT,
|
||||
`name` VARCHAR(255) NOT NULL,
|
||||
`slug` VARCHAR(255) NOT NULL UNIQUE,
|
||||
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_persian_ci;
|
||||
|
||||
-- +migrate Down
|
||||
DROP TABLE IF EXISTS `categories`;
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
-- +migrate Up
|
||||
CREATE TABLE `products` (
|
||||
`id` INT PRIMARY KEY AUTO_INCREMENT,
|
||||
`name` VARCHAR(255) NOT NULL,
|
||||
`slug` VARCHAR(255) NOT NULL UNIQUE,
|
||||
`description` TEXT,
|
||||
`price` DECIMAL(10, 2) NOT NULL,
|
||||
`stock` INT DEFAULT 0,
|
||||
`is_active` BOOLEAN DEFAULT TRUE,
|
||||
`features` JSON DEFAULT NULL,
|
||||
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`deleted_at` TIMESTAMP DEFAULT NULL,
|
||||
FOREIGN KEY (`category_id`) REFERENCES `categories` (`id`) ON DELETE SET NULL
|
||||
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_persian_ci;
|
||||
|
||||
-- +migrate Down
|
||||
DROP TABLE IF EXISTS `products`;
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
-- +migrate Up
|
||||
CREATE TABLE `category_products` (
|
||||
`category_id` INT NOT NULL,
|
||||
`product_id` INT NOT NULL,
|
||||
PRIMARY KEY (`category_id`, `product_id`),
|
||||
FOREIGN KEY (`category_id`) REFERENCES `categories` (`id`) ON DELETE CASCADE,
|
||||
FOREIGN KEY (`product_id`) REFERENCES `products` (`id`) ON DELETE CASCADE
|
||||
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_persian_ci;
|
||||
|
||||
-- +migrate Down
|
||||
DROP TABLE IF EXISTS `category_products`;
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
-- +migrate Up
|
||||
CREATE TABLE `product_images` (
|
||||
`id` INT PRIMARY KEY AUTO_INCREMENT,
|
||||
`product_id` INT NOT NULL,
|
||||
`image_path` VARCHAR(255) NOT NULL,
|
||||
`is_primary` BOOLEAN DEFAULT FALSE,
|
||||
FOREIGN KEY (`product_id`) REFERENCES `products` (`id`) ON DELETE CASCADE
|
||||
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_persian_ci;
|
||||
|
||||
-- +migrate Down
|
||||
DROP TABLE IF EXISTS `product_images`;
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package category
|
||||
|
||||
import "time"
|
||||
|
||||
type Category struct {
|
||||
ID uint
|
||||
Name string
|
||||
Slug string
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
package category
|
||||
|
|
@ -0,0 +1 @@
|
|||
package category
|
||||
|
|
@ -0,0 +1 @@
|
|||
package category
|
||||
|
|
@ -1 +0,0 @@
|
|||
package service
|
||||
|
|
@ -1 +0,0 @@
|
|||
package service
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
package product
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Product struct {
|
||||
ID uint
|
||||
Name string
|
||||
Slug string
|
||||
Description string
|
||||
Price float64
|
||||
Stock int
|
||||
IsActive bool
|
||||
Features string
|
||||
CreatedAt time.Time
|
||||
DeletedAt sql.NullTime
|
||||
}
|
||||
|
||||
type ProductImage struct {
|
||||
ID uint
|
||||
ProductID uint
|
||||
ImagePath string
|
||||
IsPrimary bool
|
||||
}
|
||||
|
||||
type CategoryProduct struct {
|
||||
CategoryID uint
|
||||
ProductID uint
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
package product
|
||||
|
|
@ -0,0 +1 @@
|
|||
package product
|
||||
|
|
@ -0,0 +1 @@
|
|||
package product
|
||||
|
|
@ -1 +0,0 @@
|
|||
package service
|
||||
|
|
@ -1 +0,0 @@
|
|||
package service
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func main() {
|
||||
fmt.Println(" Staffapp Server Starting...")
|
||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprintf(w, "Staffapp OK!")
|
||||
})
|
||||
log.Fatal(http.ListenAndServe(":8080", nil))
|
||||
}
|
||||
|
|
@ -1,13 +1,64 @@
|
|||
package database
|
||||
|
||||
import "git.gocasts.ir/ebhomengo/niki/repository/mysql"
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.gocasts.ir/ebhomengo/niki/staffapp/service"
|
||||
)
|
||||
|
||||
type DB struct {
|
||||
conn *mysql.DB
|
||||
staffs []service.Staff
|
||||
nextID int
|
||||
}
|
||||
|
||||
func New(conn *mysql.DB) *DB {
|
||||
func New() service.StaffRepository {
|
||||
return &DB{
|
||||
conn: conn,
|
||||
staffs: []service.Staff{},
|
||||
nextID: 1,
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DB) Create(staff service.Staff) (service.Staff, error) {
|
||||
staff.ID = d.nextID
|
||||
d.staffs = append(d.staffs, staff)
|
||||
d.nextID++
|
||||
return staff, nil
|
||||
}
|
||||
|
||||
func (d *DB) Get(id int) (*service.Staff, error) {
|
||||
for _, v := range d.staffs {
|
||||
if id == v.ID {
|
||||
return &v, nil
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("The user not found with id = %d", id)
|
||||
}
|
||||
|
||||
func (d *DB) List() ([]service.Staff, error) {
|
||||
return d.staffs, nil
|
||||
}
|
||||
func (d *DB) Remove(id int) error {
|
||||
for _, v := range d.staffs {
|
||||
if id == v.ID {
|
||||
d.staffs = append(d.staffs[:id], d.staffs[id+1:]...)
|
||||
return nil
|
||||
}
|
||||
|
||||
}
|
||||
return fmt.Errorf("User with ID: %d notfound", id)
|
||||
}
|
||||
func (d *DB) Update(id int, name, lastName, phoneNumber string) (*service.Staff, error) {
|
||||
for _, v := range d.staffs {
|
||||
if id == v.ID {
|
||||
v.Name = name
|
||||
v.LastName = lastName
|
||||
v.PhoneNumber = phoneNumber
|
||||
|
||||
return &v, nil
|
||||
}
|
||||
|
||||
}
|
||||
return nil, fmt.Errorf("User with this Id(%d)couldn't found for updating", id)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
package service
|
||||
|
|
@ -1 +0,0 @@
|
|||
package service
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package service
|
||||
|
||||
type Staff struct {
|
||||
ID int
|
||||
Name string
|
||||
LastName string
|
||||
PhoneNumber string
|
||||
}
|
||||
type StaffRepository interface {
|
||||
Create(staff Staff) (Staff, error)
|
||||
Get(id int) (*Staff, error)
|
||||
List() ([]Staff, error)
|
||||
Remove(id int) error
|
||||
Update(id int, name, lastName, phoneNumber string) (*Staff, error)
|
||||
}
|
||||
|
|
@ -1 +1,44 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type StaffService struct {
|
||||
repo StaffRepository
|
||||
}
|
||||
|
||||
func NewStaffService(repo StaffRepository) *StaffService {
|
||||
return &StaffService{
|
||||
repo: repo,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *StaffService) RegisterStaff(name, lastname, phone string) (Staff, error) {
|
||||
newStaff := Staff{
|
||||
Name: name,
|
||||
LastName: lastname,
|
||||
PhoneNumber: phone,
|
||||
}
|
||||
createdStaff, err := s.repo.Create(newStaff)
|
||||
if err != nil {
|
||||
return Staff{}, fmt.Errorf("Error in registring staff%w", err)
|
||||
}
|
||||
return createdStaff, nil
|
||||
}
|
||||
|
||||
func (s *StaffService) Get(id int) (*Staff, error) {
|
||||
return s.repo.Get(id)
|
||||
}
|
||||
|
||||
func (s *StaffService) List() ([]Staff, error) {
|
||||
return s.repo.List()
|
||||
}
|
||||
|
||||
func (s *StaffService) Remove(id int) error {
|
||||
return s.repo.Remove(id)
|
||||
}
|
||||
|
||||
func (s *StaffService) Update(id int, name, lastName, phoneNumber string) (*Staff, error) {
|
||||
return s.repo.Update(id, name, lastName, phoneNumber)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
package service
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2018 Amir Khazaie
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
# Jalaali
|
||||
|
||||
Golang implementation of [Jalaali JS](https://github.com/jalaali/jalaali-js) and [Jalaali Python](https://github.com/jalaali/jalaali-python) implementations of Jalaali (Jalali, Persian, Khayyami, Khorshidi, Shamsi) convertion to Gregorian calendar system and vice-versa.
|
||||
|
||||
This implementation is based on an [algorithm by Kazimierz M. Borkowski](http://www.astro.uni.torun.pl/~kb/Papers/EMP/PersianC-EMP.htm). Borkowski claims that this algorithm works correctly for 3000 years!
|
||||
|
||||
Documentation on API is available [here](https://pkg.go.dev/github.com/jalaali/go-jalaali) at Go official documentation site.
|
||||
|
||||
## Installation
|
||||
|
||||
Use `go get` on this repository:
|
||||
|
||||
```sh
|
||||
$ go get -u github.com/jalaali/go-jalaali
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
* Wrapper around Golang [time package](https://golang.org/pkg/time):
|
||||
* Call `Jalaali.Now()` to get instance of current time. You can use all function from `time` package with this wrapper.
|
||||
* Call `Jalaali.From(t)` and pass a `time` instance to it. The you can work with it the same way you work with `time` package.
|
||||
* Jalaali Formatting:
|
||||
* Call `JFormat` method of a Jalaali instance and pass it the same formatting options that is used for Golang `time` package. The output will be in Jalaali date and use persian digits and words.
|
||||
|
|
@ -0,0 +1,147 @@
|
|||
package jalaali
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
breaks = [...]int{-61, 9, 38, 199, 426, 686, 756, 818, 1111, 1181, 1210,
|
||||
1635, 2060, 2097, 2192, 2262, 2324, 2394, 2456, 3178}
|
||||
)
|
||||
|
||||
// ToJalaali converts Gregorian to Jalaali date. Error is not nil if Jalaali
|
||||
// year passed to function is not valid.
|
||||
func ToJalaali(gregorianYear int, gregorianMonth time.Month, gregorianDay int) (int, Month, int, error) {
|
||||
jy, jm, jd, err := d2j(g2d(gregorianYear, int(gregorianMonth), gregorianDay))
|
||||
return jy, Month(jm), jd, err
|
||||
}
|
||||
|
||||
// ToGregorian converts Jalaali to Gregorian date. Error is not nil if Jalaali
|
||||
// year passed to function is not valid.
|
||||
func ToGregorian(jalaaliYear int, jalaaliMonth Month, jalaaliDay int) (int, time.Month, int, error) {
|
||||
// validate the jalaali date using the utility function
|
||||
if !IsValidDate(jalaaliYear, int(jalaaliMonth), jalaaliDay) {
|
||||
return 0, 0, 0, fmt.Errorf("invalid jalaali date: year=%d, month=%d, day=%d", jalaaliYear, jalaaliMonth, jalaaliDay)
|
||||
}
|
||||
jdn, err := j2d(jalaaliYear, int(jalaaliMonth), jalaaliDay)
|
||||
if err != nil {
|
||||
return 0, 0, 0, err
|
||||
}
|
||||
|
||||
gy, gm, gd := d2g(jdn)
|
||||
return gy, time.Month(gm), gd, nil
|
||||
}
|
||||
|
||||
func j2d(jy, jm, jd int) (jdn int, err error) {
|
||||
_, gy, march, err := jalCal(jy)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return g2d(gy, 3, march) + (jm-1)*31 - div(jm, 7)*(jm-7) + jd - 1, nil
|
||||
}
|
||||
|
||||
func d2j(jdn int) (int, int, int, error) {
|
||||
gy, _, _ := d2g(jdn) // Calculate Gregorian year (gy).
|
||||
jy := gy - 621
|
||||
leap, _, march, err := jalCal(jy)
|
||||
jdn1f := g2d(gy, 3, march)
|
||||
|
||||
if err != nil {
|
||||
return 0, 0, 0, err
|
||||
}
|
||||
|
||||
// Find number of days that passed since 1 Farvardin.
|
||||
k := jdn - jdn1f
|
||||
if k >= 0 {
|
||||
if k <= 185 {
|
||||
// The first 6 months.
|
||||
jm := 1 + div(k, 31)
|
||||
jd := mod(k, 31) + 1
|
||||
return jy, jm, jd, nil
|
||||
}
|
||||
// The remaining months.
|
||||
k -= 186
|
||||
} else {
|
||||
// Previous Jalaali year.
|
||||
jy--
|
||||
k += 179
|
||||
if leap == 1 {
|
||||
k++
|
||||
}
|
||||
}
|
||||
jm := 7 + div(k, 30)
|
||||
jd := mod(k, 30) + 1
|
||||
return jy, jm, jd, nil
|
||||
}
|
||||
|
||||
func jalCal(jy int) (int, int, int, error) {
|
||||
bl, gy, leapJ, jp := len(breaks), jy+621, -14, breaks[0]
|
||||
jump := 0
|
||||
|
||||
if jy < jp || jy >= breaks[bl-1] {
|
||||
return 0, 0, 0, &ErrorInvalidYear{jy}
|
||||
}
|
||||
|
||||
// Find the limiting years for the Jalaali year jy.
|
||||
for i := 1; i < bl; i++ {
|
||||
jm := breaks[i]
|
||||
jump = jm - jp
|
||||
if jy < jm {
|
||||
break
|
||||
}
|
||||
leapJ += div(jump, 33)*8 + div(mod(jump, 33), 4)
|
||||
jp = jm
|
||||
}
|
||||
n := jy - jp
|
||||
|
||||
// Find the number of leap years from AD 621 to the beginning
|
||||
// of the current Jalaali year in the Persian calendar.
|
||||
leapJ += div(n, 33)*8 + div(mod(n, 33)+3, 4)
|
||||
if mod(jump, 33) == 4 && jump-n == 4 {
|
||||
leapJ++
|
||||
}
|
||||
|
||||
// And the same in the Gregorian calendar (until the year gy).
|
||||
leapG := div(gy, 4) - div((div(gy, 100)+1)*3, 4) - 150
|
||||
|
||||
// Determine the Gregorian date of Farvardin the 1st.
|
||||
march := 20 + leapJ - leapG
|
||||
|
||||
// Find how many years have passed since the last leap year.
|
||||
if jump-n < 6 {
|
||||
n -= jump + div(jump+4, 33)*33
|
||||
}
|
||||
leap := mod(mod(n+1, 33)-1, 4)
|
||||
if leap == -1 {
|
||||
leap = 4
|
||||
}
|
||||
|
||||
return leap, gy, march, nil
|
||||
}
|
||||
|
||||
func g2d(gy, gm, gd int) int {
|
||||
d := div((gy+div(gm-8, 6)+100100)*1461, 4) +
|
||||
div(153*mod(gm+9, 12)+2, 5) +
|
||||
gd - 34840408
|
||||
d = d - div(div(gy+100100+div(gm-8, 6), 100)*3, 4) + 752
|
||||
return d
|
||||
}
|
||||
|
||||
func d2g(jdn int) (int, int, int) {
|
||||
j := 4*jdn + 139361631
|
||||
j = j + div(div(4*jdn+183187720, 146097)*3, 4)*4 - 3908
|
||||
i := div(mod(j, 1461), 4)*5 + 308
|
||||
gd := div(mod(i, 153), 5) + 1
|
||||
gm := mod(div(i, 153), 12) + 1
|
||||
gy := div(j, 1461) - 100100 + div(8-gm, 6)
|
||||
return gy, gm, gd
|
||||
}
|
||||
|
||||
func div(a, b int) int {
|
||||
return a / b
|
||||
}
|
||||
|
||||
func mod(a, b int) int {
|
||||
return a % b
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package jalaali
|
||||
|
||||
import "fmt"
|
||||
|
||||
// ErrorNilReference is happening when a pointer is nil.
|
||||
type ErrorNilReference struct{}
|
||||
|
||||
// ErrorInvalidYear is happening when year passed is is in proper range.
|
||||
type ErrorInvalidYear struct {
|
||||
year int
|
||||
}
|
||||
|
||||
func (e *ErrorNilReference) Error() string {
|
||||
return "jalaali: reference is nil"
|
||||
}
|
||||
|
||||
func (e *ErrorInvalidYear) Error() string {
|
||||
return fmt.Sprintf("jalaali: %v is invalid year", e.year)
|
||||
}
|
||||
|
|
@ -0,0 +1,318 @@
|
|||
package jalaali
|
||||
|
||||
const (
|
||||
_ = iota
|
||||
stdLongMonth = iota + stdNeedDate // "January"
|
||||
stdMonth // "Jan"
|
||||
stdNumMonth // "1"
|
||||
stdZeroMonth // "01"
|
||||
stdLongWeekDay // "Monday"
|
||||
stdWeekDay // "Mon"
|
||||
stdDay // "2"
|
||||
stdUnderDay // "_2"
|
||||
stdZeroDay // "02"
|
||||
stdHour = iota + stdNeedClock // "15"
|
||||
stdHour12 // "3"
|
||||
stdZeroHour12 // "03"
|
||||
stdMinute // "4"
|
||||
stdZeroMinute // "04"
|
||||
stdSecond // "5"
|
||||
stdZeroSecond // "05"
|
||||
stdLongYear = iota + stdNeedDate // "2006"
|
||||
stdYear // "06"
|
||||
stdPM = iota + stdNeedClock // "PM"
|
||||
stdpm // "pm"
|
||||
stdFracSecond0 // ".0", ".00", ... , trailing zeros included
|
||||
stdFracSecond9 // ".9", ".99", ..., trailing zeros omitted
|
||||
|
||||
stdNeedDate = 1 << 8 // need month, day, year
|
||||
stdNeedClock = 2 << 8 // need hour, minute, second
|
||||
stdArgShift = 16 // extra argument in high bits, above low stdArgShift
|
||||
stdMask = 1<<stdArgShift - 1 // mask out argument
|
||||
)
|
||||
|
||||
// std0x records the std values for "01", "02", ..., "06".
|
||||
var std0x = [...]int{stdZeroMonth, stdZeroDay, stdZeroHour12, stdZeroMinute, stdZeroSecond, stdYear}
|
||||
|
||||
// JFormat gets default Golang layout string and parse put Jalaali calender information
|
||||
// into the final string and return it.
|
||||
func (j Jalaali) JFormat(layout string) (string, error) {
|
||||
const minBufSize = 64
|
||||
|
||||
bufSize := len(layout)
|
||||
if bufSize < minBufSize { // minimum buffer size
|
||||
bufSize = minBufSize
|
||||
}
|
||||
b := make([]byte, 0, len(layout))
|
||||
|
||||
b, err := j.jAppendFormat(b, layout)
|
||||
return string(b), err
|
||||
}
|
||||
|
||||
// jAppendFormat is like JFormat but appends the textual
|
||||
// representation to b and returns the extended buffer.
|
||||
func (j Jalaali) jAppendFormat(b []byte, layout string) ([]byte, error) {
|
||||
var (
|
||||
year int = -1
|
||||
month Month
|
||||
day int
|
||||
hour int = -1
|
||||
min int
|
||||
sec int
|
||||
)
|
||||
// Each iteration generates one std value.
|
||||
for layout != "" {
|
||||
prefix, std, suffix := nextStdChunk(layout)
|
||||
if prefix != "" {
|
||||
b = append(b, prefix...)
|
||||
}
|
||||
if std == 0 {
|
||||
break
|
||||
}
|
||||
layout = suffix
|
||||
|
||||
// Compute year, month, day if needed.
|
||||
if year < 0 && std&stdNeedDate != 0 {
|
||||
var err error
|
||||
year, month, day, err = ToJalaali(j.Year(), j.Month(), j.Day())
|
||||
if err != nil {
|
||||
return b, err
|
||||
}
|
||||
}
|
||||
|
||||
// Compute hour, minute, second if needed.
|
||||
if hour < 0 && std&stdNeedClock != 0 {
|
||||
hour, min, sec = j.Hour(), j.Minute(), j.Second()
|
||||
}
|
||||
|
||||
switch std & stdMask {
|
||||
case stdYear:
|
||||
y := year
|
||||
if y < 0 {
|
||||
y = -y
|
||||
}
|
||||
b = appendInt(b, y%100, 2)
|
||||
case stdLongYear:
|
||||
b = appendInt(b, year, 4)
|
||||
case stdMonth, stdLongMonth:
|
||||
m := month.String()
|
||||
b = append(b, m...)
|
||||
case stdNumMonth:
|
||||
b = appendInt(b, int(month), 0)
|
||||
case stdZeroMonth:
|
||||
b = appendInt(b, int(month), 2)
|
||||
case stdWeekDay, stdLongWeekDay:
|
||||
s := Weekday((int(j.Weekday()) + 1) % 7).String()
|
||||
b = append(b, s...)
|
||||
case stdDay:
|
||||
b = appendInt(b, day, 0)
|
||||
case stdUnderDay:
|
||||
if day < 10 {
|
||||
b = append(b, ' ')
|
||||
}
|
||||
b = appendInt(b, day, 0)
|
||||
case stdZeroDay:
|
||||
b = appendInt(b, day, 2)
|
||||
case stdHour:
|
||||
b = appendInt(b, hour, 2)
|
||||
case stdHour12:
|
||||
// Noon is 12PM, midnight is 12AM.
|
||||
hr := hour % 12
|
||||
if hr == 0 {
|
||||
hr = 12
|
||||
}
|
||||
b = appendInt(b, hr, 0)
|
||||
case stdZeroHour12:
|
||||
// Noon is 12PM, midnight is 12AM.
|
||||
hr := hour % 12
|
||||
if hr == 0 {
|
||||
hr = 12
|
||||
}
|
||||
b = appendInt(b, hr, 2)
|
||||
case stdMinute:
|
||||
b = appendInt(b, min, 0)
|
||||
case stdZeroMinute:
|
||||
b = appendInt(b, min, 2)
|
||||
case stdSecond:
|
||||
b = appendInt(b, sec, 0)
|
||||
case stdZeroSecond:
|
||||
b = appendInt(b, sec, 2)
|
||||
case stdPM, stdpm:
|
||||
if hour >= 12 {
|
||||
b = append(b, "بعدازظهر"...)
|
||||
} else {
|
||||
b = append(b, "قبلازظهر"...)
|
||||
}
|
||||
case stdFracSecond0, stdFracSecond9:
|
||||
b = formatNano(b, uint(j.Nanosecond()), std>>stdArgShift, std&stdMask == stdFracSecond9)
|
||||
}
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
|
||||
// nextStdChunk finds the first occurrence of a std string in
|
||||
// layout and returns the text before, the std string, and the text after.
|
||||
func nextStdChunk(layout string) (prefix string, std int, suffix string) {
|
||||
for i := 0; i < len(layout); i++ {
|
||||
switch c := int(layout[i]); c {
|
||||
case 'J': // January, Jan
|
||||
if len(layout) >= i+3 && layout[i:i+3] == "Jan" {
|
||||
if len(layout) >= i+7 && layout[i:i+7] == "January" {
|
||||
return layout[0:i], stdLongMonth, layout[i+7:]
|
||||
}
|
||||
if !startsWithLowerCase(layout[i+3:]) {
|
||||
return layout[0:i], stdMonth, layout[i+3:]
|
||||
}
|
||||
}
|
||||
|
||||
case 'M': // Monday, Mon
|
||||
if layout[i:i+3] == "Mon" {
|
||||
if len(layout) >= i+6 && layout[i:i+6] == "Monday" {
|
||||
return layout[0:i], stdLongWeekDay, layout[i+6:]
|
||||
}
|
||||
if !startsWithLowerCase(layout[i+3:]) {
|
||||
return layout[0:i], stdWeekDay, layout[i+3:]
|
||||
}
|
||||
}
|
||||
|
||||
case '0': // 01, 02, 03, 04, 05, 06
|
||||
if len(layout) >= i+2 && '1' <= layout[i+1] && layout[i+1] <= '6' {
|
||||
return layout[0:i], std0x[layout[i+1]-'1'], layout[i+2:]
|
||||
}
|
||||
|
||||
case '1': // 15, 1
|
||||
if len(layout) >= i+2 && layout[i+1] == '5' {
|
||||
return layout[0:i], stdHour, layout[i+2:]
|
||||
}
|
||||
return layout[0:i], stdNumMonth, layout[i+1:]
|
||||
|
||||
case '2': // 2006, 2
|
||||
if len(layout) >= i+4 && layout[i:i+4] == "2006" {
|
||||
return layout[0:i], stdLongYear, layout[i+4:]
|
||||
}
|
||||
return layout[0:i], stdDay, layout[i+1:]
|
||||
|
||||
case '_': // _2, _2006
|
||||
if len(layout) >= i+2 && layout[i+1] == '2' {
|
||||
//_2006 is really a literal _, followed by stdLongYear
|
||||
if len(layout) >= i+5 && layout[i+1:i+5] == "2006" {
|
||||
return layout[0 : i+1], stdLongYear, layout[i+5:]
|
||||
}
|
||||
return layout[0:i], stdUnderDay, layout[i+2:]
|
||||
}
|
||||
|
||||
case '3':
|
||||
return layout[0:i], stdHour12, layout[i+1:]
|
||||
|
||||
case '4':
|
||||
return layout[0:i], stdMinute, layout[i+1:]
|
||||
|
||||
case '5':
|
||||
return layout[0:i], stdSecond, layout[i+1:]
|
||||
|
||||
case 'P': // PM
|
||||
if len(layout) >= i+2 && layout[i+1] == 'M' {
|
||||
return layout[0:i], stdPM, layout[i+2:]
|
||||
}
|
||||
|
||||
case 'p': // pm
|
||||
if len(layout) >= i+2 && layout[i+1] == 'm' {
|
||||
return layout[0:i], stdpm, layout[i+2:]
|
||||
}
|
||||
|
||||
case '.': // .000 or .999 - repeated digits for fractional seconds.
|
||||
if i+1 < len(layout) && (layout[i+1] == '0' || layout[i+1] == '9') {
|
||||
ch := layout[i+1]
|
||||
j := i + 1
|
||||
for j < len(layout) && layout[j] == ch {
|
||||
j++
|
||||
}
|
||||
// String of digits must end here - only fractional second is all digits.
|
||||
if !isDigit(layout, j) {
|
||||
std := stdFracSecond0
|
||||
if layout[i+1] == '9' {
|
||||
std = stdFracSecond9
|
||||
}
|
||||
std |= (j - (i + 1)) << stdArgShift
|
||||
return layout[0:i], std, layout[j:]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return layout, 0, ""
|
||||
}
|
||||
|
||||
// startsWithLowerCase reports whether the string has a lower-case letter at the beginning.
|
||||
// Its purpose is to prevent matching strings like "Month" when looking for "Mon".
|
||||
func startsWithLowerCase(str string) bool {
|
||||
if len(str) == 0 {
|
||||
return false
|
||||
}
|
||||
c := str[0]
|
||||
return 'a' <= c && c <= 'z'
|
||||
}
|
||||
|
||||
// isDigit reports whether s[i] is in range and is a decimal digit.
|
||||
func isDigit(s string, i int) bool {
|
||||
if len(s) <= i {
|
||||
return false
|
||||
}
|
||||
c := s[i]
|
||||
return '0' <= c && c <= '9'
|
||||
}
|
||||
|
||||
// appendInt appends the decimal form of x to b and returns the result.
|
||||
// If the decimal form (excluding sign) is shorter than width, the result is padded with leading 0's.
|
||||
// Duplicates functionality in strconv, but avoids dependency.
|
||||
func appendInt(b []byte, x int, width int) []byte {
|
||||
u := uint(x)
|
||||
if x < 0 {
|
||||
b = append(b, '-')
|
||||
u = uint(-x)
|
||||
}
|
||||
|
||||
// Assemble decimal in reverse order.
|
||||
var buf [20]rune
|
||||
i := len(buf)
|
||||
for u >= 10 {
|
||||
i--
|
||||
q := u / 10
|
||||
buf[i] = rune('۰' + u - q*10)
|
||||
u = q
|
||||
}
|
||||
i--
|
||||
buf[i] = rune('۰' + u)
|
||||
|
||||
// Add 0-padding.
|
||||
for w := len(buf) - i; w < width; w++ {
|
||||
b = append(b, []byte("۰")...)
|
||||
}
|
||||
|
||||
return append(b, []byte(string(buf[i:]))...)
|
||||
}
|
||||
|
||||
// formatNano appends a fractional second, as nanoseconds, to b
|
||||
// and returns the result.
|
||||
func formatNano(b []byte, nanosec uint, n int, trim bool) []byte {
|
||||
u := nanosec
|
||||
var buf [9]rune
|
||||
for start := len(buf); start > 0; {
|
||||
start--
|
||||
buf[start] = rune(u%10 + '۰')
|
||||
u /= 10
|
||||
}
|
||||
|
||||
if n > 9 {
|
||||
n = 9
|
||||
}
|
||||
if trim {
|
||||
for n > 0 && buf[n-1] == '۰' {
|
||||
n--
|
||||
}
|
||||
if n == 0 {
|
||||
return b
|
||||
}
|
||||
}
|
||||
b = append(b, '.')
|
||||
return append(b, []byte(string(buf[:n]))...)
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
module github.com/jalaali/go-jalaali
|
||||
|
||||
go 1.13
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
package jalaali
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// A simple wrapper around Golang default time package. You have all the functionality of
|
||||
// default time package and functionalities needed for Jalaali calender.
|
||||
type Jalaali struct {
|
||||
time.Time
|
||||
}
|
||||
|
||||
// From initialize new instance of Jalaali from a time instance.
|
||||
func From(t time.Time) Jalaali {
|
||||
return Jalaali{t}
|
||||
}
|
||||
|
||||
// Now with return Jalaali instance of current time.
|
||||
func Now() Jalaali {
|
||||
return From(time.Now())
|
||||
}
|
||||
|
||||
// A Month specifies a month of the year (Farvardin = 1, ...).
|
||||
type Month int
|
||||
|
||||
const (
|
||||
Farvardin Month = 1 + iota
|
||||
Ordibehesht
|
||||
Khordad
|
||||
Tir
|
||||
Mordad
|
||||
Shahrivar
|
||||
Mehr
|
||||
Aban
|
||||
Azar
|
||||
Dey
|
||||
Bahman
|
||||
Esfand
|
||||
)
|
||||
|
||||
var months = []string{
|
||||
"فروردین", "اردیبهشت", "خرداد",
|
||||
"تیر", "مرداد", "شهریور",
|
||||
"مهر", "آبان", "آذر",
|
||||
"دی", "بهمن", "اسفند",
|
||||
}
|
||||
|
||||
func (m Month) String() string {
|
||||
if Farvardin <= m && m <= Esfand {
|
||||
return months[m-1]
|
||||
}
|
||||
return "%!Month(" + strconv.Itoa(int(m)) + ")"
|
||||
}
|
||||
|
||||
// A Weekday specifies a day of the week (Shanbe = 0, ...).
|
||||
type Weekday int
|
||||
|
||||
const (
|
||||
Shanbe Weekday = iota
|
||||
IekShanbe
|
||||
DoShanbe
|
||||
SeShanbe
|
||||
ChaharShanbe
|
||||
PanjShanbe
|
||||
Jome
|
||||
)
|
||||
|
||||
var days = []string{
|
||||
"شنبه", "یکشنبه", "دوشنبه", "سهشنبه", "چهارشنبه", "پنجشنبه", "جمعه",
|
||||
}
|
||||
|
||||
func (d Weekday) String() string {
|
||||
if Shanbe <= d && d <= Jome {
|
||||
return days[d]
|
||||
}
|
||||
return "%!Weekday(" + strconv.Itoa(int(d)) + ")"
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,188 @@
|
|||
package jalaali
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestFromYMD(t *testing.T) {
|
||||
tests := []struct {
|
||||
gy, gm, gd, jy, jm, jd int
|
||||
}{
|
||||
{1981, 8, 17, 1360, 5, 26},
|
||||
{2013, 1, 10, 1391, 10, 21},
|
||||
{2014, 8, 4, 1393, 5, 13},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
y, m, d, err := ToJalaali(test.gy, time.Month(test.gm), test.gd)
|
||||
if err != nil {
|
||||
t.Errorf("%v", err)
|
||||
} else if y != test.jy || m != Month(test.jm) || d != test.jd {
|
||||
t.Errorf("Expected %v/%v/%v got %v/%v%v.", test.jy, test.jm, test.jd, y, m, d)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestToGregorian(t *testing.T) {
|
||||
tests := []struct {
|
||||
jy, jm, jd, gy, gm, gd int
|
||||
}{
|
||||
{1360, 5, 26, 1981, 8, 17},
|
||||
{1391, 10, 21, 2013, 1, 10},
|
||||
{1393, 5, 13, 2014, 8, 4},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
y, m, d, err := ToGregorian(test.jy, Month(test.jm), test.jd)
|
||||
if err != nil {
|
||||
t.Errorf("%v", err)
|
||||
} else if y != test.gy || m != time.Month(test.gm) || d != test.gd {
|
||||
t.Errorf("Expected %v/%v/%v got %v/%v%v.", test.gy, test.gm, test.gd, y, m, d)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestIsValidDate(t *testing.T) {
|
||||
tests := []struct {
|
||||
y, m, d int
|
||||
ok bool
|
||||
}{
|
||||
{-62, 12, 29, false},
|
||||
{-61, 1, 1, true},
|
||||
{3178, 1, 1, false},
|
||||
{3177, 12, 29, true},
|
||||
{1393, 0, 1, false},
|
||||
{1393, 13, 1, false},
|
||||
{1393, 1, 0, false},
|
||||
{1393, 1, 32, false},
|
||||
{1393, 1, 31, true},
|
||||
{1393, 11, 31, false},
|
||||
{1393, 11, 30, true},
|
||||
{1393, 12, 30, false},
|
||||
{1393, 12, 29, true},
|
||||
{1395, 12, 30, true},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
valid := IsValidDate(test.y, test.m, test.d)
|
||||
if valid != test.ok {
|
||||
calculated, actual := "", " not"
|
||||
if test.ok {
|
||||
calculated, actual = " not", ""
|
||||
}
|
||||
t.Errorf("%v/%v/%v is%v valid date but considered%v valid.",
|
||||
test.y, test.m, test.d, actual, calculated)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsLeapYear(t *testing.T) {
|
||||
tests := []struct {
|
||||
year int
|
||||
leap bool
|
||||
}{
|
||||
{1393, false},
|
||||
{1394, false},
|
||||
{1395, true},
|
||||
{1396, false},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
leap, err := IsLeapYear(test.year)
|
||||
if err != nil {
|
||||
t.Errorf("%v", err)
|
||||
} else if leap != test.leap {
|
||||
calculated, actual := "", " not"
|
||||
if leap {
|
||||
calculated, actual = " not", ""
|
||||
}
|
||||
t.Errorf("%v is%v leap but considered%v leap.", test.year, actual, calculated)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMonthLength(t *testing.T) {
|
||||
tests := []struct {
|
||||
y, m, ml int
|
||||
}{
|
||||
{1393, 1, 31},
|
||||
{1393, 4, 31},
|
||||
{1393, 6, 31},
|
||||
{1393, 7, 30},
|
||||
{1393, 10, 30},
|
||||
{1393, 12, 29},
|
||||
{1394, 12, 29},
|
||||
{1395, 12, 30},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
calculated, err := MonthLength(test.y, test.m)
|
||||
if err != nil {
|
||||
t.Errorf("%v", err)
|
||||
} else if calculated != test.ml {
|
||||
t.Errorf("Length of %v/%v month is %v but considered %v.",
|
||||
test.y, test.m, test.ml, calculated)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestJFormat(t *testing.T) {
|
||||
iran, _ := time.LoadLocation("Asia/Tehran")
|
||||
|
||||
tests := []struct {
|
||||
time time.Time
|
||||
format []string
|
||||
result []string
|
||||
}{
|
||||
{
|
||||
time.Date(2001, 1, 1, 1, 1, 1, 1, iran),
|
||||
[]string{
|
||||
"2006 06", // Year formatting
|
||||
"January Jan 1 01", // Month formatting
|
||||
"Monday Mon 2 _2 02", // Day formatting
|
||||
"15 3 03 4 04 5 05 PM pm", // Hour, Minute, Second formatting
|
||||
".0 .00 .000 .000000 .000000000 .9 .99 .999 .999999 .999999999", // Nanosecond formatting
|
||||
},
|
||||
[]string{
|
||||
"۱۳۷۹ ۷۹", // Year formatting
|
||||
"دی دی ۱۰ ۱۰", // Month formatting
|
||||
"دوشنبه دوشنبه ۱۲ ۱۲ ۱۲", // Day formatting
|
||||
"۰۱ ۱ ۰۱ ۱ ۰۱ ۱ ۰۱ قبلازظهر قبلازظهر", // Hour, Minute, Second formatting
|
||||
".۰ .۰۰ .۰۰۰ .۰۰۰۰۰۰ .۰۰۰۰۰۰۰۰۱ .۰۰۰۰۰۰۰۰۱", // Nanosecond formatting
|
||||
},
|
||||
}, {
|
||||
time.Date(2001, 2, 3, 15, 17, 1, 999999999, iran),
|
||||
[]string{
|
||||
"2006 06", // Year formatting
|
||||
"January Jan 1 01", // Month formatting
|
||||
"Monday Mon 2 _2 02", // Day formatting
|
||||
"15 3 03 4 04 5 05 PM pm", // Hour, Minute, Second formatting
|
||||
".0 .00 .000 .000000 .000000000 .9 .99 .999 .999999 .999999999", // Nanosecond formatting
|
||||
},
|
||||
[]string{
|
||||
"۱۳۷۹ ۷۹", // Year formatting
|
||||
"بهمن بهمن ۱۱ ۱۱", // Month formatting
|
||||
"شنبه شنبه ۱۵ ۱۵ ۱۵", // Day formatting
|
||||
"۱۵ ۳ ۰۳ ۱۷ ۱۷ ۱ ۰۱ بعدازظهر بعدازظهر", // Hour, Minute, Second formatting
|
||||
".۹ .۹۹ .۹۹۹ .۹۹۹۹۹۹ .۹۹۹۹۹۹۹۹۹ .۹ .۹۹ .۹۹۹ .۹۹۹۹۹۹ .۹۹۹۹۹۹۹۹۹", // Nanosecond formatting
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
j := From(test.time)
|
||||
|
||||
for f := range test.format {
|
||||
result, err := j.JFormat(test.format[f])
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if result != test.result[f] {
|
||||
t.Error("Bad formatting for test as index: ", i, "\nWanted: ", test.result[f], "\nGot: ", result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
package jalaali
|
||||
|
||||
import "strings"
|
||||
|
||||
var enToFa = strings.NewReplacer(
|
||||
"0", "۰",
|
||||
"1", "۱",
|
||||
"2", "۲",
|
||||
"3", "۳",
|
||||
"4", "۴",
|
||||
"5", "۵",
|
||||
"6", "۶",
|
||||
"7", "۷",
|
||||
"8", "۸",
|
||||
"9", "۹",
|
||||
)
|
||||
|
||||
// IsValidDate take Jalaali date and return true if it is valid,
|
||||
// otherwise false.
|
||||
func IsValidDate(jy, jm, jd int) bool {
|
||||
d, err := MonthLength(jy, jm)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return -61 <= jy && jy <= 3177 &&
|
||||
1 <= jm && jm <= 12 &&
|
||||
1 <= jd && jd <= d
|
||||
}
|
||||
|
||||
// MonthLength take Jalaali date and return length of that specific
|
||||
// month. Error is not nil if Jalaali year passed to function is not valid.
|
||||
func MonthLength(jy, jm int) (int, error) {
|
||||
if jm <= 6 {
|
||||
return 31, nil
|
||||
} else if jm <= 11 {
|
||||
return 30, nil
|
||||
}
|
||||
|
||||
leap, err := IsLeapYear(jy)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
} else if leap {
|
||||
return 30, nil
|
||||
}
|
||||
return 29, nil
|
||||
}
|
||||
|
||||
// IsLeapYear take a Jalaali year and return true if it is leap year. Error
|
||||
// is not nil if Jalaali year passed to function is not valid.
|
||||
func IsLeapYear(jy int) (bool, error) {
|
||||
leap, _, _, err := jalCal(jy)
|
||||
return leap == 0, err
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: gomod
|
||||
directory: /
|
||||
schedule:
|
||||
interval: weekly
|
||||
open-pull-requests-limit: 99
|
||||
- package-ecosystem: github-actions
|
||||
directory: /
|
||||
schedule:
|
||||
interval: weekly
|
||||
open-pull-requests-limit: 99
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
# changes to documentation generation
|
||||
"area/docs-generation":
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: 'doc/**'
|
||||
|
||||
# changes to the core cobra command
|
||||
"area/cobra-command":
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: ['./cobra.go', './cobra_test.go', './*command*.go']
|
||||
|
||||
# changes made to command flags/args
|
||||
"area/flags":
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: './args*.go'
|
||||
|
||||
# changes to Github workflows
|
||||
"area/github":
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: '.github/**'
|
||||
|
||||
# changes to shell completions
|
||||
"area/shell-completion":
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: './*completions*'
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
name: "Pull Request Labeler"
|
||||
on:
|
||||
- pull_request_target
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
triage:
|
||||
permissions:
|
||||
contents: read # for actions/labeler to determine modified files
|
||||
pull-requests: write # for actions/labeler to add labels to PRs
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/labeler@v5
|
||||
with:
|
||||
repo-token: "${{ github.token }}"
|
||||
|
||||
|
|
@ -0,0 +1,125 @@
|
|||
name: Test
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
GO111MODULE: on
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
|
||||
|
||||
lic-headers:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- run: >-
|
||||
docker run
|
||||
-v $(pwd):/wrk -w /wrk
|
||||
ghcr.io/google/addlicense
|
||||
-c 'The Cobra Authors'
|
||||
-y '2013-2023'
|
||||
-l apache
|
||||
-ignore '.github/**'
|
||||
-check
|
||||
.
|
||||
|
||||
|
||||
golangci-lint:
|
||||
permissions:
|
||||
contents: read # for actions/checkout to fetch code
|
||||
pull-requests: read # for golangci/golangci-lint-action to fetch pull requests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '^1.22'
|
||||
check-latest: true
|
||||
cache: true
|
||||
|
||||
- uses: golangci/golangci-lint-action@v8.0.0
|
||||
with:
|
||||
version: latest
|
||||
args: --verbose
|
||||
|
||||
|
||||
test-unix:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
platform:
|
||||
- ubuntu
|
||||
- macOS
|
||||
go:
|
||||
- 17
|
||||
- 18
|
||||
- 19
|
||||
- 20
|
||||
- 21
|
||||
- 22
|
||||
- 23
|
||||
- 24
|
||||
name: '${{ matrix.platform }} | 1.${{ matrix.go }}.x'
|
||||
runs-on: ${{ matrix.platform }}-latest
|
||||
steps:
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.${{ matrix.go }}.x
|
||||
cache: true
|
||||
|
||||
- run: |
|
||||
export GOBIN=$HOME/go/bin
|
||||
go install github.com/kyoh86/richgo@latest
|
||||
go install github.com/mitchellh/gox@latest
|
||||
|
||||
- run: RICHGO_FORCE_COLOR=1 PATH=$HOME/go/bin/:$PATH make richtest
|
||||
|
||||
|
||||
test-win:
|
||||
name: MINGW64
|
||||
defaults:
|
||||
run:
|
||||
shell: msys2 {0}
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
|
||||
- shell: bash
|
||||
run: git config --global core.autocrlf input
|
||||
|
||||
- uses: msys2/setup-msys2@v2
|
||||
with:
|
||||
msystem: MINGW64
|
||||
update: true
|
||||
install: >
|
||||
git
|
||||
make
|
||||
unzip
|
||||
mingw-w64-x86_64-go
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/go/pkg/mod
|
||||
key: ${{ runner.os }}-${{ matrix.go }}-${{ hashFiles('**/go.sum') }}
|
||||
restore-keys: ${{ runner.os }}-${{ matrix.go }}-
|
||||
|
||||
- run: |
|
||||
export GOBIN=$HOME/go/bin
|
||||
go install github.com/kyoh86/richgo@latest
|
||||
go install github.com/mitchellh/gox@latest
|
||||
|
||||
- run: RICHGO_FORCE_COLOR=1 PATH=$HOME/go/bin:$PATH make richtest
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
||||
*.o
|
||||
*.a
|
||||
*.so
|
||||
|
||||
# Folders
|
||||
_obj
|
||||
_test
|
||||
|
||||
# Architecture specific extensions/prefixes
|
||||
*.[568vq]
|
||||
[568vq].out
|
||||
|
||||
*.cgo1.go
|
||||
*.cgo2.c
|
||||
_cgo_defun.c
|
||||
_cgo_gotypes.go
|
||||
_cgo_export.*
|
||||
|
||||
_testmain.go
|
||||
|
||||
# Vim files https://github.com/github/gitignore/blob/master/Global/Vim.gitignore
|
||||
# swap
|
||||
[._]*.s[a-w][a-z]
|
||||
[._]s[a-w][a-z]
|
||||
# session
|
||||
Session.vim
|
||||
# temporary
|
||||
.netrwhist
|
||||
*~
|
||||
# auto-generated tag files
|
||||
tags
|
||||
|
||||
*.exe
|
||||
cobra.test
|
||||
bin
|
||||
|
||||
.idea/
|
||||
*.iml
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
# Copyright 2013-2023 The Cobra Authors
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
version: "2"
|
||||
|
||||
run:
|
||||
timeout: 5m
|
||||
|
||||
formatters:
|
||||
enable:
|
||||
- gofmt
|
||||
- goimports
|
||||
|
||||
linters:
|
||||
default: none
|
||||
enable:
|
||||
#- bodyclose
|
||||
#- depguard
|
||||
#- dogsled
|
||||
#- dupl
|
||||
- errcheck
|
||||
#- exhaustive
|
||||
#- funlen
|
||||
#- gochecknoinits
|
||||
- goconst
|
||||
- gocritic
|
||||
#- gocyclo
|
||||
#- goprintffuncname
|
||||
- gosec
|
||||
- govet
|
||||
- ineffassign
|
||||
#- lll
|
||||
- misspell
|
||||
#- mnd
|
||||
#- nakedret
|
||||
#- noctx
|
||||
- nolintlint
|
||||
#- rowserrcheck
|
||||
- staticcheck
|
||||
- unconvert
|
||||
#- unparam
|
||||
- unused
|
||||
#- whitespace
|
||||
exclusions:
|
||||
presets:
|
||||
- common-false-positives
|
||||
- legacy
|
||||
- std-error-handling
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
Steve Francia <steve.francia@gmail.com>
|
||||
Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
|
||||
Fabiano Franz <ffranz@redhat.com> <contact@fabianofranz.com>
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
## Cobra User Contract
|
||||
|
||||
### Versioning
|
||||
Cobra will follow a steady release cadence. Non breaking changes will be released as minor versions quarterly. Patch bug releases are at the discretion of the maintainers. Users can expect security patch fixes to be released within relatively short order of a CVE becoming known. For more information on security patch fixes see the CVE section below. Releases will follow [Semantic Versioning](https://semver.org/). Users tracking the Master branch should expect unpredictable breaking changes as the project continues to move forward. For stability, it is highly recommended to use a release.
|
||||
|
||||
### Backward Compatibility
|
||||
We will maintain two major releases in a moving window. The N-1 release will only receive bug fixes and security updates and will be dropped once N+1 is released.
|
||||
|
||||
### Deprecation
|
||||
Deprecation of Go versions or dependent packages will only occur in major releases. To reduce the change of this taking users by surprise, any large deprecation will be preceded by an announcement in the [#cobra slack channel](https://gophers.slack.com/archives/CD3LP1199) and an Issue on Github.
|
||||
|
||||
### CVE
|
||||
Maintainers will make every effort to release security patches in the case of a medium to high severity CVE directly impacting the library. The speed in which these patches reach a release is up to the discretion of the maintainers. A low severity CVE may be a lower priority than a high severity one.
|
||||
|
||||
### Communication
|
||||
Cobra maintainers will use GitHub issues and the [#cobra slack channel](https://gophers.slack.com/archives/CD3LP1199) as the primary means of communication with the community. This is to foster open communication with all users and contributors.
|
||||
|
||||
### Breaking Changes
|
||||
Breaking changes are generally allowed in the master branch, as this is the branch used to develop the next release of Cobra.
|
||||
|
||||
There may be times, however, when master is closed for breaking changes. This is likely to happen as we near the release of a new version.
|
||||
|
||||
Breaking changes are not allowed in release branches, as these represent minor versions that have already been released. These version have consumers who expect the APIs, behaviors, etc, to remain stable during the lifetime of the patch stream for the minor release.
|
||||
|
||||
Examples of breaking changes include:
|
||||
- Removing or renaming exported constant, variable, type, or function.
|
||||
- Updating the version of critical libraries such as `spf13/pflag`, `spf13/viper` etc...
|
||||
- Some version updates may be acceptable for picking up bug fixes, but maintainers must exercise caution when reviewing.
|
||||
|
||||
There may, at times, need to be exceptions where breaking changes are allowed in release branches. These are at the discretion of the project's maintainers, and must be carefully considered before merging.
|
||||
|
||||
### CI Testing
|
||||
Maintainers will ensure the Cobra test suite utilizes the current supported versions of Golang.
|
||||
|
||||
### Disclaimer
|
||||
Changes to this document and the contents therein are at the discretion of the maintainers.
|
||||
None of the contents of this document are legally binding in any way to the maintainers or the users.
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
# Contributing to Cobra
|
||||
|
||||
Thank you so much for contributing to Cobra. We appreciate your time and help.
|
||||
Here are some guidelines to help you get started.
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
Be kind and respectful to the members of the community. Take time to educate
|
||||
others who are seeking help. Harassment of any kind will not be tolerated.
|
||||
|
||||
## Questions
|
||||
|
||||
If you have questions regarding Cobra, feel free to ask it in the community
|
||||
[#cobra Slack channel][cobra-slack]
|
||||
|
||||
## Filing a bug or feature
|
||||
|
||||
1. Before filing an issue, please check the existing issues to see if a
|
||||
similar one was already opened. If there is one already opened, feel free
|
||||
to comment on it.
|
||||
1. If you believe you've found a bug, please provide detailed steps of
|
||||
reproduction, the version of Cobra and anything else you believe will be
|
||||
useful to help troubleshoot it (e.g. OS environment, environment variables,
|
||||
etc...). Also state the current behavior vs. the expected behavior.
|
||||
1. If you'd like to see a feature or an enhancement please open an issue with
|
||||
a clear title and description of what the feature is and why it would be
|
||||
beneficial to the project and its users.
|
||||
|
||||
## Submitting changes
|
||||
|
||||
1. CLA: Upon submitting a Pull Request (PR), contributors will be prompted to
|
||||
sign a CLA. Please sign the CLA :slightly_smiling_face:
|
||||
1. Tests: If you are submitting code, please ensure you have adequate tests
|
||||
for the feature. Tests can be run via `go test ./...` or `make test`.
|
||||
1. Since this is golang project, ensure the new code is properly formatted to
|
||||
ensure code consistency. Run `make all`.
|
||||
|
||||
### Quick steps to contribute
|
||||
|
||||
1. Fork the project.
|
||||
1. Download your fork to your PC (`git clone https://github.com/your_username/cobra && cd cobra`)
|
||||
1. Create your feature branch (`git checkout -b my-new-feature`)
|
||||
1. Make changes and run tests (`make test`)
|
||||
1. Add them to staging (`git add .`)
|
||||
1. Commit your changes (`git commit -m 'Add some feature'`)
|
||||
1. Push to the branch (`git push origin my-new-feature`)
|
||||
1. Create new pull request
|
||||
|
||||
<!-- Links -->
|
||||
[cobra-slack]: https://gophers.slack.com/archives/CD3LP1199
|
||||
|
|
@ -0,0 +1,174 @@
|
|||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
maintainers:
|
||||
- spf13
|
||||
- johnSchnake
|
||||
- jpmcb
|
||||
- marckhouzam
|
||||
inactive:
|
||||
- anthonyfok
|
||||
- bep
|
||||
- bogem
|
||||
- broady
|
||||
- eparis
|
||||
- jharshman
|
||||
- wfernandes
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
BIN="./bin"
|
||||
SRC=$(shell find . -name "*.go")
|
||||
|
||||
ifeq (, $(shell which golangci-lint))
|
||||
$(warning "could not find golangci-lint in $(PATH), run: curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh")
|
||||
endif
|
||||
|
||||
.PHONY: fmt lint test install_deps clean
|
||||
|
||||
default: all
|
||||
|
||||
all: fmt test
|
||||
|
||||
fmt:
|
||||
$(info ******************** checking formatting ********************)
|
||||
@test -z $(shell gofmt -l $(SRC)) || (gofmt -d $(SRC); exit 1)
|
||||
|
||||
lint:
|
||||
$(info ******************** running lint tools ********************)
|
||||
golangci-lint run -v
|
||||
|
||||
test: install_deps
|
||||
$(info ******************** running tests ********************)
|
||||
go test -v ./...
|
||||
|
||||
richtest: install_deps
|
||||
$(info ******************** running tests with kyoh86/richgo ********************)
|
||||
richgo test -v ./...
|
||||
|
||||
install_deps:
|
||||
$(info ******************** downloading dependencies ********************)
|
||||
go get -v ./...
|
||||
|
||||
clean:
|
||||
rm -rf $(BIN)
|
||||
|
|
@ -0,0 +1,133 @@
|
|||
<div align="center">
|
||||
<a href="https://cobra.dev">
|
||||
<img width="512" height="535" alt="cobra-logo" src="https://github.com/user-attachments/assets/c8bf9aad-b5ae-41d3-8899-d83baec10af8" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
Cobra is a library for creating powerful modern CLI applications.
|
||||
|
||||
<a href="https://cobra.dev">Visit Cobra.dev for extensive documentation</a>
|
||||
|
||||
|
||||
Cobra is used in many Go projects such as [Kubernetes](https://kubernetes.io/),
|
||||
[Hugo](https://gohugo.io), and [GitHub CLI](https://github.com/cli/cli) to
|
||||
name a few. [This list](site/content/projects_using_cobra.md) contains a more extensive list of projects using Cobra.
|
||||
|
||||
[](https://github.com/spf13/cobra/actions?query=workflow%3ATest)
|
||||
[](https://pkg.go.dev/github.com/spf13/cobra)
|
||||
[](https://goreportcard.com/report/github.com/spf13/cobra)
|
||||
[](https://gophers.slack.com/archives/CD3LP1199)
|
||||
<hr>
|
||||
<div align="center" markdown="1">
|
||||
<sup>Supported by:</sup>
|
||||
<br>
|
||||
<br>
|
||||
<a href="https://www.warp.dev/cobra">
|
||||
<img alt="Warp sponsorship" width="400" src="https://github.com/user-attachments/assets/ab8dd143-b0fd-4904-bdc5-dd7ecac94eae">
|
||||
</a>
|
||||
|
||||
### [Warp, the AI terminal for devs](https://www.warp.dev/cobra)
|
||||
[Try Cobra in Warp today](https://www.warp.dev/cobra)<br>
|
||||
|
||||
</div>
|
||||
<hr>
|
||||
|
||||
# Overview
|
||||
|
||||
Cobra is a library providing a simple interface to create powerful modern CLI
|
||||
interfaces similar to git & go tools.
|
||||
|
||||
Cobra provides:
|
||||
* Easy subcommand-based CLIs: `app server`, `app fetch`, etc.
|
||||
* Fully POSIX-compliant flags (including short & long versions)
|
||||
* Nested subcommands
|
||||
* Global, local and cascading flags
|
||||
* Intelligent suggestions (`app srver`... did you mean `app server`?)
|
||||
* Automatic help generation for commands and flags
|
||||
* Grouping help for subcommands
|
||||
* Automatic help flag recognition of `-h`, `--help`, etc.
|
||||
* Automatically generated shell autocomplete for your application (bash, zsh, fish, powershell)
|
||||
* Automatically generated man pages for your application
|
||||
* Command aliases so you can change things without breaking them
|
||||
* The flexibility to define your own help, usage, etc.
|
||||
* Optional seamless integration with [viper](https://github.com/spf13/viper) for 12-factor apps
|
||||
|
||||
# Concepts
|
||||
|
||||
Cobra is built on a structure of commands, arguments & flags.
|
||||
|
||||
**Commands** represent actions, **Args** are things and **Flags** are modifiers for those actions.
|
||||
|
||||
The best applications read like sentences when used, and as a result, users
|
||||
intuitively know how to interact with them.
|
||||
|
||||
The pattern to follow is
|
||||
`APPNAME VERB NOUN --ADJECTIVE`
|
||||
or
|
||||
`APPNAME COMMAND ARG --FLAG`.
|
||||
|
||||
A few good real world examples may better illustrate this point.
|
||||
|
||||
In the following example, 'server' is a command, and 'port' is a flag:
|
||||
|
||||
hugo server --port=1313
|
||||
|
||||
In this command we are telling Git to clone the url bare.
|
||||
|
||||
git clone URL --bare
|
||||
|
||||
## Commands
|
||||
|
||||
Command is the central point of the application. Each interaction that
|
||||
the application supports will be contained in a Command. A command can
|
||||
have children commands and optionally run an action.
|
||||
|
||||
In the example above, 'server' is the command.
|
||||
|
||||
[More about cobra.Command](https://pkg.go.dev/github.com/spf13/cobra#Command)
|
||||
|
||||
## Flags
|
||||
|
||||
A flag is a way to modify the behavior of a command. Cobra supports
|
||||
fully POSIX-compliant flags as well as the Go [flag package](https://golang.org/pkg/flag/).
|
||||
A Cobra command can define flags that persist through to children commands
|
||||
and flags that are only available to that command.
|
||||
|
||||
In the example above, 'port' is the flag.
|
||||
|
||||
Flag functionality is provided by the [pflag
|
||||
library](https://github.com/spf13/pflag), a fork of the flag standard library
|
||||
which maintains the same interface while adding POSIX compliance.
|
||||
|
||||
# Installing
|
||||
Using Cobra is easy. First, use `go get` to install the latest version
|
||||
of the library.
|
||||
|
||||
```
|
||||
go get -u github.com/spf13/cobra@latest
|
||||
```
|
||||
|
||||
Next, include Cobra in your application:
|
||||
|
||||
```go
|
||||
import "github.com/spf13/cobra"
|
||||
```
|
||||
|
||||
# Usage
|
||||
`cobra-cli` is a command line program to generate cobra applications and command files.
|
||||
It will bootstrap your application scaffolding to rapidly
|
||||
develop a Cobra-based application. It is the easiest way to incorporate Cobra into your application.
|
||||
|
||||
It can be installed by running:
|
||||
|
||||
```
|
||||
go install github.com/spf13/cobra-cli@latest
|
||||
```
|
||||
|
||||
For complete details on using the Cobra-CLI generator, please read [The Cobra Generator README](https://github.com/spf13/cobra-cli/blob/main/README.md)
|
||||
|
||||
For complete details on using the Cobra library, please read [The Cobra User Guide](site/content/user_guide.md).
|
||||
|
||||
# License
|
||||
|
||||
Cobra is released under the Apache 2.0 license. See [LICENSE.txt](LICENSE.txt)
|
||||
|
|
@ -0,0 +1,105 @@
|
|||
# Security Policy
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
The `cobra` maintainers take security issues seriously and
|
||||
we appreciate your efforts to _**responsibly**_ disclose your findings.
|
||||
We will make every effort to swiftly respond and address concerns.
|
||||
|
||||
To report a security vulnerability:
|
||||
|
||||
1. **DO NOT** create a public GitHub issue for the vulnerability!
|
||||
2. **DO NOT** create a public GitHub Pull Request with a fix for the vulnerability!
|
||||
3. Send an email to `cobra-security@googlegroups.com`.
|
||||
4. Include the following details in your report:
|
||||
- Description of the vulnerability
|
||||
- Steps to reproduce
|
||||
- Potential impact of the vulnerability (to your downstream project, to the Go ecosystem, etc.)
|
||||
- Any potential mitigations you've already identified
|
||||
5. Allow up to 7 days for an initial response.
|
||||
You should receive an acknowledgment of your report and an estimated timeline for a fix.
|
||||
6. (Optional) If you have a fix and would like to contribute your patch, please work
|
||||
directly with the maintainers via `cobra-security@googlegroups.com` to
|
||||
coordinate pushing the patch to GitHub, cutting a new release, and disclosing the change.
|
||||
|
||||
## Response Process
|
||||
|
||||
When a security vulnerability report is received, the `cobra` maintainers will:
|
||||
|
||||
1. Confirm receipt of the vulnerability report within 7 days.
|
||||
2. Assess the report to determine if it constitutes a security vulnerability.
|
||||
3. If confirmed, assign the vulnerability a severity level and create a timeline for addressing it.
|
||||
4. Develop and test a fix.
|
||||
5. Patch the vulnerability and make a new GitHub release: the maintainers will coordinate disclosure with the reporter.
|
||||
6. Create a new GitHub Security Advisory to inform the broader Go ecosystem
|
||||
|
||||
## Disclosure Policy
|
||||
|
||||
The `cobra` maintainers follow a coordinated disclosure process:
|
||||
|
||||
1. Security vulnerabilities will be addressed as quickly as possible.
|
||||
2. A CVE (Common Vulnerabilities and Exposures) identifier will be requested for significant vulnerabilities
|
||||
that are within `cobra` itself.
|
||||
3. Once a fix is ready, the maintainers will:
|
||||
- Release a new version containing the fix.
|
||||
- Update the security advisory with details about the vulnerability.
|
||||
- Credit the reporter (unless they wish to remain anonymous).
|
||||
- Credit the fixer (unless they wish to remain anonymous, this may be the same as the reporter).
|
||||
- Announce the vulnerability through appropriate channels
|
||||
(GitHub Security Advisory, mailing lists, GitHub Releases, etc.)
|
||||
|
||||
## Supported Versions
|
||||
|
||||
Security fixes will typically only be released for the most recent major release.
|
||||
|
||||
## Upstream Security Issues
|
||||
|
||||
`cobra` generally will not accept vulnerability reports that originate in upstream
|
||||
dependencies. I.e., if there is a problem in Go code that `cobra` depends on,
|
||||
it is best to engage that project's maintainers and owners.
|
||||
|
||||
This security policy primarily pertains only to `cobra` itself but if you believe you've
|
||||
identified a problem that originates in an upstream dependency and is being widely
|
||||
distributed by `cobra`, please follow the disclosure procedure above: the `cobra`
|
||||
maintainers will work with you to determine the severity and ecosystem impact.
|
||||
|
||||
## Security Updates and CVEs
|
||||
|
||||
Information about known security vulnerabilities and CVEs affecting `cobra` will
|
||||
be published as GitHub Security Advisories at
|
||||
https://github.com/spf13/cobra/security/advisories.
|
||||
|
||||
All users are encouraged to watch the repository and upgrade promptly when
|
||||
security releases are published.
|
||||
|
||||
## `cobra` Security Best Practices for Users
|
||||
|
||||
When using `cobra` in your CLIs, the `cobra` maintainers recommend the following:
|
||||
|
||||
1. Always use the latest version of `cobra`.
|
||||
2. [Use Go modules](https://go.dev/blog/using-go-modules) for dependency management.
|
||||
3. Always use the latest possible version of Go.
|
||||
|
||||
## Security Best Practices for Contributors
|
||||
|
||||
When contributing to `cobra`:
|
||||
|
||||
1. Be mindful of security implications when adding new features or modifying existing ones.
|
||||
2. Be aware of `cobra`'s extremely large reach: it is used in nearly every Go CLI
|
||||
(like Kubernetes, Docker, Prometheus, etc. etc.)
|
||||
3. Write tests that explicitly cover edge cases and potential issues.
|
||||
4. If you discover a security issue while working on `cobra`, please report it
|
||||
following the process above rather than opening a public pull request or issue that
|
||||
addresses the vulnerability.
|
||||
5. Take personal sec-ops seriously and secure your GitHub account: use [two-factor authentication](https://docs.github.com/en/authentication/securing-your-account-with-two-factor-authentication-2fa),
|
||||
[sign your commits with a GPG or SSH key](https://docs.github.com/en/authentication/managing-commit-signature-verification/about-commit-signature-verification),
|
||||
etc.
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
The `cobra` maintainers would like to thank all security researchers and
|
||||
community members who help keep cobra, its users, and the entire Go ecosystem secure through responsible disclosures!!
|
||||
|
||||
---
|
||||
|
||||
*This security policy is inspired by the [Open Web Application Security Project (OWASP)](https://owasp.org/) guidelines and security best practices.*
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
// Copyright 2013-2023 The Cobra Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package cobra
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
const (
|
||||
activeHelpMarker = "_activeHelp_ "
|
||||
// The below values should not be changed: programs will be using them explicitly
|
||||
// in their user documentation, and users will be using them explicitly.
|
||||
activeHelpEnvVarSuffix = "ACTIVE_HELP"
|
||||
activeHelpGlobalEnvVar = configEnvVarGlobalPrefix + "_" + activeHelpEnvVarSuffix
|
||||
activeHelpGlobalDisable = "0"
|
||||
)
|
||||
|
||||
// AppendActiveHelp adds the specified string to the specified array to be used as ActiveHelp.
|
||||
// Such strings will be processed by the completion script and will be shown as ActiveHelp
|
||||
// to the user.
|
||||
// The array parameter should be the array that will contain the completions.
|
||||
// This function can be called multiple times before and/or after completions are added to
|
||||
// the array. Each time this function is called with the same array, the new
|
||||
// ActiveHelp line will be shown below the previous ones when completion is triggered.
|
||||
func AppendActiveHelp(compArray []Completion, activeHelpStr string) []Completion {
|
||||
return append(compArray, fmt.Sprintf("%s%s", activeHelpMarker, activeHelpStr))
|
||||
}
|
||||
|
||||
// GetActiveHelpConfig returns the value of the ActiveHelp environment variable
|
||||
// <PROGRAM>_ACTIVE_HELP where <PROGRAM> is the name of the root command in upper
|
||||
// case, with all non-ASCII-alphanumeric characters replaced by `_`.
|
||||
// It will always return "0" if the global environment variable COBRA_ACTIVE_HELP
|
||||
// is set to "0".
|
||||
func GetActiveHelpConfig(cmd *Command) string {
|
||||
activeHelpCfg := os.Getenv(activeHelpGlobalEnvVar)
|
||||
if activeHelpCfg != activeHelpGlobalDisable {
|
||||
activeHelpCfg = os.Getenv(activeHelpEnvVar(cmd.Root().Name()))
|
||||
}
|
||||
return activeHelpCfg
|
||||
}
|
||||
|
||||
// activeHelpEnvVar returns the name of the program-specific ActiveHelp environment
|
||||
// variable. It has the format <PROGRAM>_ACTIVE_HELP where <PROGRAM> is the name of the
|
||||
// root command in upper case, with all non-ASCII-alphanumeric characters replaced by `_`.
|
||||
func activeHelpEnvVar(name string) string {
|
||||
return configEnvVar(name, activeHelpEnvVarSuffix)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue