commit 4ff98a40d424a8157d20034abc28a449993fbc6d Author: selamanapps Date: Sat May 2 02:59:10 2026 +0300 feat: initial go-faker - Ethiopian fake data generator - High-performance batch data generation (~150k records/sec) - Seeded randomness for reproducible load tests - Rich Ethiopian data: names, phones (+251), cities, regions - Four data types: person, address, product, analytics - Three modes: CLI, Go library, HTTP API - Project skill available in .skills/go-faker/SKILL.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b92aa21 --- /dev/null +++ b/.gitignore @@ -0,0 +1,30 @@ +# Binaries +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Build output +gofaker +bin/ + +# Test binary +*.test + +# Output of go coverage +*.out + +# Dependency directories +vendor/ + +# IDE +.idea/ +.vscode/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..9a1fbbd --- /dev/null +++ b/README.md @@ -0,0 +1,175 @@ +# Go-Faker + +High-performance Ethiopian fake data generator for load testing and development. + +## Features + +- **Seeded randomness** - Reproducible datasets across runs +- **Batch generation** - Generate thousands of records in a single call +- **Rich Ethiopian data** - Names, cities, regions, phone numbers (+251), and more +- **Two modes** - Use as a Go library or HTTP API server +- **Fast** - ~150k records/second throughput + +## Quick Start + +### CLI + +```bash +# Generate 10 person records +./gofaker -count 10 -type person + +# Generate 100 product records with specific fields +./gofaker -count 100 -type product -fields name,price,sku + +# Use a seed for reproducible data +./gofaker -count 50 -type person -seed my-test-seed +``` + +### API Server + +```bash +# Start server +./gofaker -server -addr :8080 + +# Generate via HTTP +curl -X POST http://localhost:8080/generate \ + -H "Content-Type: application/json" \ + -d '{"seed":"test","count":10,"type":"person","fields":["first_name","phone","email"]}' +``` + +### Library + +```go +import "go-faker" + +f := faker.New("my-seed") + +// Generate single record +person := f.GeneratePerson([]string{"first_name", "phone", "email"}) + +// Generate batch +persons := f.GenerateBatch(1000, "person", []string{"first_name", "email", "city"}) +``` + +## Data Types + +### person + +| Field | Description | +|-------|-------------| +| first_name | Ethiopian first name | +| last_name | Ethiopian last name | +| full_name | First + last name | +| phone | Ethiopian mobile (+2519x...) | +| email | Generated email address | +| gender | Male/Female | +| age | 18-90 | +| language | Ethiopian language | +| city | Ethiopian city | +| region | Ethiopian region | +| nationality | Ethiopian/nearby | +| occupation | Job title | +| company | Ethiopian company | +| blood_type | Blood type | +| date_of_birth | YYYY-MM-DD | +| username | Generated username | +| password | Secure random password | + +### address + +| Field | Description | +|-------|-------------| +| street | Street name | +| city | City | +| region | Region | +| sub_city | Sub-city (Addis Ababa) | +| postal_code | 4-digit code | +| zone | Zone number | +| woreda | Woreda | +| kebele | Kebele number | +| house_number | House identifier | +| latitude | GPS coordinate | +| longitude | GPS coordinate | + +### product + +| Field | Description | +|-------|-------------| +| name | Product name | +| category | Product category | +| price | Price in ETB | +| sku | Stock keeping unit | +| brand | Brand name | +| description | Product description | +| weight | Weight in kg | +| stock | Stock quantity | +| expiry_date | Future date | +| manufactured_date | Past date | + +### analytics + +| Field | Description | +|-------|-------------| +| user_id | user-XXXXX | +| event | Event type | +| timestamp | RFC3339 timestamp | +| amount | Transaction amount | +| currency | ETB | +| session_id | Session identifier | +| page_url | Page URL | +| ip_address | IP address (10.x.x.x) | +| device_type | Device type | +| browser | Browser name | +| os | Operating system | +| country | Country | +| referrer | Referrer URL | + +## API Reference + +### POST /generate + +Generate fake data. + +**Request:** +```json +{ + "seed": "optional-seed-string", + "count": 100, + "type": "person", + "fields": ["first_name", "email", "phone"] +} +``` + +**Response:** +```json +{ + "data": [ + {"first_name": "Tadesse", "email": "Tadesse@email.com", "phone": "+251911234567"} + ], + "meta": { + "count": 100, + "seed": "optional-seed-string", + "generated_at": "2026-05-02T10:00:00Z" + }, + "format": "json" +} +``` + +### GET /fields + +List all valid fields per data type. + +### GET /health + +Health check endpoint. + +## Performance + +| Records | Time | Rate | +|---------|------|------| +| 10,000 | 0.4s | ~25k/s | +| 100,000 | 0.7s | ~143k/s | + +## License + +MIT \ No newline at end of file diff --git a/api/handlers.go b/api/handlers.go new file mode 100644 index 0000000..fd28fcb --- /dev/null +++ b/api/handlers.go @@ -0,0 +1,100 @@ +package api + +import ( + "encoding/json" + "net/http" + "time" + + "go-faker/generator" + "go-faker/types" +) + +type Handler struct { + faker *generator.Faker +} + +func NewHandler(seed string) *Handler { + return &Handler{ + faker: generator.New(seed), + } +} + +func (h *Handler) Generate(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + var req types.GenerateRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "Invalid request body: "+err.Error(), http.StatusBadRequest) + return + } + defer r.Body.Close() + + if req.Count <= 0 || req.Count > 100000 { + http.Error(w, "Count must be between 1 and 100000", http.StatusBadRequest) + return + } + + if req.Seed == "" { + req.Seed = time.Now().UTC().Format(time.RFC3339Nano) + } + + faker := generator.New(req.Seed) + + validFields, ok := types.ValidFields[req.Type] + if !ok { + http.Error(w, "Invalid data type", http.StatusBadRequest) + return + } + + if len(req.Fields) == 0 { + req.Fields = validFields + } + + fieldSet := make(map[string]bool) + for _, f := range validFields { + fieldSet[f] = true + } + for _, f := range req.Fields { + if !fieldSet[f] { + http.Error(w, "Invalid field: "+f, http.StatusBadRequest) + return + } + } + + data := faker.GenerateBatch(req.Count, req.Type, req.Fields) + + response := types.GenerateResponse{ + Data: data, + Format: "json", + Meta: types.ResponseMeta{ + Count: req.Count, + Seed: req.Seed, + GeneratedAt: time.Now().UTC().Format(time.RFC3339), + }, + } + + w.Header().Set("Content-Type", "application/json") + w.Header().Set("Access-Control-Allow-Origin", "*") + json.NewEncoder(w).Encode(response) +} + +func (h *Handler) Health(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(map[string]string{"status": "ok"}) +} + +func (h *Handler) Fields(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Header().Set("Access-Control-Allow-Origin", "*") + json.NewEncoder(w).Encode(types.ValidFields) +} + +func (h *Handler) RegisterRoutes(mux *http.ServeMux) { + mux.HandleFunc("/generate", h.Generate) + mux.HandleFunc("/fields", h.Fields) + mux.HandleFunc("/health", h.Health) +} \ No newline at end of file diff --git a/api/server.go b/api/server.go new file mode 100644 index 0000000..9e62450 --- /dev/null +++ b/api/server.go @@ -0,0 +1,43 @@ +package api + +import ( + "log" + "net/http" + "os" + "os/signal" + "syscall" +) + +type Server struct { + addr string + mux *http.ServeMux +} + +func NewServer(addr string) *Server { + mux := http.NewServeMux() + return &Server{addr: addr, mux: mux} +} + +func (s *Server) Start() error { + handler := NewHandler("default-seed") + handler.RegisterRoutes(s.mux) + + server := &http.Server{ + Addr: s.addr, + Handler: s.mux, + } + + go func() { + log.Printf("Go-Faker API server starting on %s", s.addr) + if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { + log.Fatalf("Server failed: %v", err) + } + }() + + quit := make(chan os.Signal, 1) + signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) + <-quit + + log.Println("Shutting down server...") + return server.Close() +} \ No newline at end of file diff --git a/cmd/gofaker/main.go b/cmd/gofaker/main.go new file mode 100644 index 0000000..5944337 --- /dev/null +++ b/cmd/gofaker/main.go @@ -0,0 +1,81 @@ +package main + +import ( + "encoding/json" + "flag" + "fmt" + "os" + + "go-faker/api" + "go-faker/generator" + "go-faker/types" +) + +var ( + serverMode bool + addr string + seed string + count int + dataType string + fieldsStr string +) + +func init() { + flag.BoolVar(&serverMode, "server", false, "Run as HTTP API server") + flag.StringVar(&addr, "addr", ":8080", "Server address") + flag.StringVar(&seed, "seed", "default", "Random seed for reproducible data") + flag.IntVar(&count, "count", 10, "Number of records to generate") + flag.StringVar(&dataType, "type", "person", "Data type: person, address, product, analytics") + flag.StringVar(&fieldsStr, "fields", "", "Comma-separated fields (empty = all)") +} + +func main() { + flag.Parse() + + if serverMode { + server := api.NewServer(addr) + if err := server.Start(); err != nil { + fmt.Fprintf(os.Stderr, "Server error: %v\n", err) + os.Exit(1) + } + return + } + + dt := types.DataType(dataType) + validFields, ok := types.ValidFields[dt] + if !ok { + fmt.Fprintf(os.Stderr, "Invalid type: %s. Valid types: person, address, product, analytics\n", dataType) + os.Exit(1) + } + + var fields []string + if fieldsStr == "" { + fields = validFields + } else { + fields = splitFields(fieldsStr) + } + + faker := generator.New(seed) + results := faker.GenerateBatch(count, dt, fields) + + enc := json.NewEncoder(os.Stdout) + enc.SetIndent("", " ") + if err := enc.Encode(results); err != nil { + fmt.Fprintf(os.Stderr, "JSON encode error: %v\n", err) + os.Exit(1) + } +} + +func splitFields(s string) []string { + var result []string + start := 0 + for i := 0; i <= len(s); i++ { + if i == len(s) || s[i] == ',' { + if start < i { + result = append(result, s[start:i]) + } + start = i + 1 + } + } + return result +} \ No newline at end of file diff --git a/data/ethiopian.go b/data/ethiopian.go new file mode 100644 index 0000000..d7486a8 --- /dev/null +++ b/data/ethiopian.go @@ -0,0 +1,163 @@ +package data + +var EthiopianFirstNames = []string{ + "Abebe", "Abera", "Abel", "Abraham", "Abrham", "Adane", "Admas", "Alem", "Alemayehu", + "Alex", "Almaz", "Amare", "Amir", "Anna", "Asrat", "Aster", "Ayana", "Bekele", "Belete", + "Bethlehem", "Birtukan", "Bogale", "Chala", "Chaltu", "Daniel", "Dagmawi", "Dawit", "Delina", + "Deribe", "Diriba", "Duncan", "Eleni", "Elias", "Ermias", "Esete", "Frehiwot", "Gabriel", + "Gashaw", "Genet", "Getachew", "Girma", "Gitanjali", "Gobena", "Gordon", "Haben", "Hailemariam", + "Haimanot", "Hana", "Helen", "Henok", "Hirut", "Hiwot", "Jamal", "Jerome", "Jimma", "Kaleab", + "Kaleb", "Kassa", "Kebede", "Kifle", "Kiros", "Kumneger", "Lalem", "Lidya", "Lily", "Mahlet", + "Makda", "Malaku", "Mamo", "Manahil", "Mariam", "Markos", "Marta", "Mekdes", + "Mekonnen", "Melaku", "Melat", "Melese", "Mengistu", "Meron", "Michele", "Mihret", + "Mikael", "Mulu", "Nathanael", "Negash", "Nega", "Netsanet", "Newer", "Nigus", "Nikol", + "Paul", "Philip", "Rahel", "Samrawit", "Samuel", "Sara", "Selam", "Sisay", "Solomon", "Sofia", + "Tadesse", "Tamrat", "Tania", "Tarik", "Tatiana", "Tebeje", "Tefera", "Tenagne", "Tenna", + "Tigist", "Tirgo", "Tobias", "Tsigereda", "Voke", "Wallelign", "Wolde", "Woldegeorgis", + "Yared", "Yasmin", "Yecats", "Yohannes", "Zelalem", "Zeleke", "Zewditu", "Zinaye", "Zinash", +} + +var EthiopianLastNames = []string{ + "Abebe", "Abera", "Abitew", "Admas", "Alemayehu", "Alemu", "Asnakech", "Bekele", "Belete", + "Berhanu", "Birhanu", "Birhane", "Bogale", "Chala", "Cheneke", "Dale", "Daniel", "Dembel", + "Deressa", "Desalegn", "Dessalegn", "Dibaba", "Diriba", "Dubale", "Dulo", "Eshete", "Fekadu", + "Feleke", "Fikadu", "Fikremariam", "Fre", "G/Medhin", "Gashaw", "Gebre", "Gebrekidan", + "Gebremariam", "Gebremedhin", "Gemechu", "Getachew", "Girma", "Gobena", "Guta", "Hailu", + "Haimanot", "Hall", "Jamal", "Jember", "Kassa", "Kassaye", "Kebede", "Kumssa", "Lema", + "Lemma", "Mekonnen", "Melaku", "Melkamu", "Mengistu", "Mersha", "Michele", "Mekdes", + "Molla", "Mulat", "Negeri", "Nega", "Olana", "Oumer", "Ramos", "Saba", "Seifu", "Sisay", + "Solomon", "Tadesse", "Tafesse", "Taye", "Tefera", "Teklu", "Temesgen", "Tessema", "Tigabe", + "Tigist", "Tola", "Wakjira", "Wolde", "Woldegeorgis", "Woldesemayat", "Yohannes", "Zenebe", + "Zewde", "Zewdie", "Zinabu", +} + +var EthiopianCities = []string{ + "Addis Ababa", "Dire Dawa", "Harar", "Adama", "Hawassa", "Mekelle", "Gondar", "Bahir Dar", + "Semera", "Asosa", " Gambela", "Jijiga", "Akordat", "Axum", "Lalibela", "Bahr Dar", + "Jimma", "Woliso", "Bishoftu", "Adwa", "Debre Markos", "Debre Tabor", "Dessie", "Dolo Ado", + "East Wollega", "Gambela", "Gojjam", "Gondar", "Harar", "Hosaena", "Jijiga", "Jimma", + "Kaffa", "Metu", "Nekemte", "Arba Minch", "Shashamane", "Wolaita Sodo", "Dilla", "Mizan Teferi", + "Sawla", "Kelem Welega", "West Wellega", "East Wellega", "Kaffa", "Gedeo", "Wolita", + "Alaba", "Konso", "Dawro", "Bench", "Sheka", "Mareko", +} + +var EthiopianRegions = []string{ + "Addis Ababa", "Afar", "Amhara", "Benishangul-Gumuz", "Central Somalia", "Dire Dawa", + "Gambela", "Harari", "Oromia", "SNNPR", "Somalia", "Tigray", +} + +var EthiopianSubCities = map[string][]string{ + "Addis Ababa": {"Akaki", "Bole", "Gullele", "Kirkos", "Kolfe", "Lideta", "Lemmi", "Yeka", "Nifassilk", "Addis Ketema"}, + "Dire Dawa": {"Dire Dawa", "Gildessa", "Mekonnen"}, + "Harar": {"Harar", "Jigil", "Shah", + "Aboker", "Sinje"}, +} + +var EthiopianWoredas = []string{ + "Adama", "Agarfa", "Akaki", "Alaba", "Alamata", "Areka", "Asossa", "Awasa", "Axum", + " Bahir Dar", "Bale", "Bonga", "Burayu", "Chiro", "Dabra", "Dalocha", "Dangila", "Deder", + "Dembel", "Dessie", "Dilla", "Dolo Ado", "Dubti", "East Wollega", "Endasilasie", "Fentale", + "Gimbi", "Gondar", "Grawa", "Hagere Selam", "Harar", "Hosaena", "Jijiga", "Jimma", + "Karrayu", "Kofele", "Konga", "Lalibela", "Liben", "Mekelle", "Mekonnen", "Mizan", + "Mojo", "Negele", "Nekemte", "Otona", "Sawla", "Semera", "Shashamane", "Shire", "Sodo", + "Sululta", "Tepi", "Welenchiti", "Woliso", "Wondo", "Yirgalem", "Zeway", +} + +var EthiopianStreets = []string{ + " Bole Road", "Africa Avenue", "Cameroon Street", "Nigeria Street", "Kenya Street", + "Egypt Street", "Congo Street", "Sudan Avenue", "Guinea Street", "Tanzania Road", + "Uganda Street", "Rwanda Avenue", "Ghana Street", "Sierra Leone Road", "Liberia Street", + "Morocco Avenue", "Algeria Street", "Tunisia Road", "Libya Street", "Mauritania Avenue", + "Ethiopia Street", "National Palace Road", "Church Road", "Market Street", "Station Road", + "Railway Street", "Airport Road", "University Avenue", "Hospital Road", "School Street", + "Police Station Road", "Fire Brigade Street", "Post Office Avenue", "Telecom Street", + "Industrial Road", "Factory Street", "Workshop Road", "Garage Avenue", "Hotel Street", +} + +var PhonePrefixes = []string{ + "+25191", "+25192", "+25193", "+25194", "+25195", "+25196", "+25197", "+25198", "+25199", + "+25170", "+25171", "+25172", "+25173", "+25174", "+25175", "+25176", "+25177", "+25178", "+25179", + "+25111", "+25112", "+25113", "+25114", "+25115", "+25116", "+25117", "+25118", "+25119", +} + +var BloodTypes = []string{"A+", "A-", "B+", "B-", "AB+", "AB-", "O+", "O-"} + +var Genders = []string{"Male", "Female"} + +var Languages = []string{ + "Amharic", "Oromiffa", "Tigrinya", "Somali", "Arabic", "Wolayta", "Affar", "Hadiyya", + "Gedeo", "Kambaata", "Konso", "Burji", "Daasenech", "Mursi", "Hamer", "Nyangatom", + "Kara", "Maale", "Dinka", "Nuwer", "Jie", "Toposa", "Anyuak", "Shilluk", "Mundang", + "Koma", "Opta", "Shekkacho", "Maji", "Bear", "Weyto", "Kusto", "Mesmes", "Kewama", "Gumuz", + "Berta", "Hadendowa", "Blin", "Kunama", "Nara", "Rashaida", +} + +var Occupations = []string{ + "Software Engineer", "Doctor", "Teacher", "Nurse", "Accountant", "Engineer", "Architect", + "Lawyer", "Journalist", "Police Officer", "Soldier", "Farmer", "Trader", "Business Owner", + "Government Employee", "Banker", "Electrician", "Plumber", "Carpenter", "Driver", + "Chef", "Waiter", "Shop Keeper", "Tailor", "Hairdresser", "Mechanic", "Welder", + "Pharmacist", "Dentist", "Veterinarian", "Pilot", "Flight Attendant", "Hotel Manager", + "Tour Guide", "Taxi Driver", "Motorcycle Rider", "Street Vendor", "Laborer", "Security Guard", + "Secretary", "Receptionist", "Data Entry Clerk", "HR Manager", "Marketing Manager", + "Sales Representative", "Consultant", "Researcher", "Professor", "Student", +} + +var Companies = []string{ + "Ethio Telecom", "Ethiopian Airlines", "Commercial Bank of Ethiopia", "Dashen Bank", + "Awash Bank", "United Bank", "Bank of Abyssinia", "Nib International Bank", + "Zemen Bank", "Addis Bank", "Oromia International Bank", "Bunna International Bank", + "Construction and Business Bank", "Development Bank of Ethiopia", "Ethiopian Investment Holdings", + "Ethiopian Electric Power", "Ethiopian Petroleum Supply Enterprise", "Ethiopian Mining Corporation", + "East African Holding", "Midroc Ethiopia", "Lydford Mining", "Kality Metal & Engineering", + "Al-Madani Group", "Beshale Group", "Julphar Ethiopia", "Halal Pharmaceutical", + "Africa Educational Services", "Yegna Gebeya", "Shoa Agro Industry", "Ahadu Bank", + "HiLCoFe", "Fasika Shopping Center", "Berecha Supermarket", "Kale Heywet Chemist", + "Addis continental institute", "Ripple Addis", "Afro-Dollar Group", "Summit Consulting", +} + +var Nationalities = []string{"Ethiopian", "Eritrean", "Kenyan", "Ugandan", "Somali", "Sudanese", "Djibouti"} + +var ProductCategories = []string{ + "Electronics", "Clothing", "Food & Beverages", "Home & Garden", "Sports & Outdoors", + "Toys & Games", "Books & Stationery", "Health & Beauty", "Automotive", "Industrial", + "Agricultural", "Construction", "Medical", "Office Supplies", "Pet Supplies", +} + +var ProductNames = []string{ + "Injera Pot", "Coffee Roaster", "Berbere Spice Set", "Tej Honey Wine", "Coffee Table", + "Wooden Stool", "Cotton Scarf", "Silk Shawl", "Leather Sandals", "Brass Jewelry Box", + "Adder Compass", "Coffee Bean Grinder", "Ceramic Cup Set", "Woven Basket", "Palm Leaf Fan", + "Spice Mortar", "Clay Oven", "Bamboo Mat", "Recycled Glass Vase", "Metal Tribal Art", +} + +var Brands = []string{ + "Addis Brand", "EthioCraft", "Oromia Style", "Tigray Treasures", "Amhara Arts", + "ESM", "Zemen Essentials", "Bole Fashion", "Harar Heritage", "Gondar Gold", + "National", "Panasonic", "Samsung", "LG", "Hisense", "Nokia", "Tecno", "Infinix", + "Titan", "Casio", "Local Made", "Handicraft Co", +} + +var EventTypes = []string{ + "page_view", "click", "purchase", "add_to_cart", "remove_from_cart", "signup", + "login", "logout", "search", "filter", "sort", "share", "download", "upload", + "form_submit", "video_play", "video_pause", "video_complete", "scroll_depth", + "session_start", "session_end", "error", "notification", "message_sent", "message_received", +} + +var DeviceTypes = []string{"Desktop", "Mobile", "Tablet", "Smart TV", "Smart Watch"} + +var Browsers = []string{"Chrome", "Firefox", "Safari", "Edge", "Opera", "Samsung Internet", "UC Browser"} + +var OperatingSystems = []string{"Windows", "macOS", "Linux", "Android", "iOS", "Chrome OS", "Ubuntu"} + +var Countries = []string{ + "Ethiopia", "Kenya", "Uganda", "Tanzania", "Rwanda", "Burundi", "South Sudan", "Sudan", + "Eritrea", "Djibouti", "Somalia", "Egypt", "Libya", "Algeria", "Morocco", "Tunisia", +} + +var Referrers = []string{ + "google.com", "facebook.com", "twitter.com", "instagram.com", "linkedin.com", + "youtube.com", "bing.com", "yahoo.com", "direct", "bookmark", "email_campaign", + "referral_link", "partner_site", "affiliate_network", +} \ No newline at end of file diff --git a/faker.go b/faker.go new file mode 100644 index 0000000..60c7ddd --- /dev/null +++ b/faker.go @@ -0,0 +1,50 @@ +package faker + +import ( + "go-faker/generator" + "go-faker/types" +) + +type Faker struct { + inner *generator.Faker +} + +func New(seed string) *Faker { + return &Faker{inner: generator.New(seed)} +} + +func (f *Faker) GeneratePerson(fields []string) map[string]any { + return f.inner.GeneratePerson(fields) +} + +func (f *Faker) GenerateAddress(fields []string) map[string]any { + return f.inner.GenerateAddress(fields) +} + +func (f *Faker) GenerateProduct(fields []string) map[string]any { + return f.inner.GenerateProduct(fields) +} + +func (f *Faker) GenerateAnalytics(fields []string) map[string]any { + return f.inner.GenerateAnalytics(fields) +} + +func (f *Faker) GenerateBatch(count int, dataType types.DataType, fields []string) []map[string]any { + return f.inner.GenerateBatch(count, dataType, fields) +} + +func (f *Faker) GeneratePersonStruct(count int, fields []string) []map[string]any { + return f.inner.GenerateBatch(count, types.TypePerson, fields) +} + +func (f *Faker) GenerateAddressStruct(count int, fields []string) []map[string]any { + return f.inner.GenerateBatch(count, types.TypeAddress, fields) +} + +func (f *Faker) GenerateProductStruct(count int, fields []string) []map[string]any { + return f.inner.GenerateBatch(count, types.TypeProduct, fields) +} + +func (f *Faker) GenerateAnalyticsStruct(count int, fields []string) []map[string]any { + return f.inner.GenerateBatch(count, types.TypeAnalytics, fields) +} \ No newline at end of file diff --git a/generator/generator.go b/generator/generator.go new file mode 100644 index 0000000..3fec89a --- /dev/null +++ b/generator/generator.go @@ -0,0 +1,316 @@ +package generator + +import ( + "crypto/md5" + "fmt" + "math/rand" + "time" + + "go-faker/data" + "go-faker/types" +) + +type Faker struct { + rng *rand.Rand +} + +func New(seed string) *Faker { + s := hashSeed(seed) + src := rand.NewSource(s) + return &Faker{rng: rand.New(src)} +} + +func hashSeed(seed string) int64 { + h := md5.Sum([]byte(seed)) + var result int64 + for i := 0; i < 8; i++ { + result = result<<8 | int64(h[i]) + } + return result +} + +func (f *Faker) generateUUID() string { + b := make([]byte, 16) + f.rng.Read(b) + return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:]) +} + +func (f *Faker) generateSKU() string { + return fmt.Sprintf("SKU-%08d", f.rng.Intn(99999999)) +} + +func (f *Faker) generatePhone() string { + prefix := data.PhonePrefixes[f.rng.Intn(len(data.PhonePrefixes))] + number := fmt.Sprintf("%07d", f.rng.Intn(9999999)) + return prefix + number +} + +func (f *Faker) generateEmail(firstName, lastName string) string { + domains := []string{"gmail.com", "yahoo.com", "outlook.com", "hotmail.com", "icloud.com", "telecom.net.et", "ethiotelecom.et"} + domain := domains[f.rng.Intn(len(domains))] + separator := []string{".", "_", ""} + sep := separator[f.rng.Intn(len(separator))] + + variants := []string{ + fmt.Sprintf("%s%s%s@%s", firstName, sep, lastName, domain), + fmt.Sprintf("%s.%s@%s", firstName, lastName, domain), + fmt.Sprintf("%s%d@%s", firstName, f.rng.Intn(999), domain), + fmt.Sprintf("%s_%s%d@%s", firstName, lastName, f.rng.Intn(99), domain), + } + return variants[f.rng.Intn(len(variants))] +} + +func (f *Faker) generateAge() int { + return f.rng.Intn(72) + 18 +} + +func (f *Faker) generateDateOfBirth() string { + now := time.Now() + maxAge := 72 + minAge := 18 + maxBirthDate := now.AddDate(-minAge, 0, 0) + minBirthDate := now.AddDate(-maxAge, 0, 0) + + unixMin := minBirthDate.Unix() + unixMax := maxBirthDate.Unix() + unixBirth := unixMin + f.rng.Int63n(unixMax-unixMin) + + t := time.Unix(unixBirth, 0) + return t.Format("2006-01-02") +} + +func (f *Faker) generatePastDate(maxDaysAgo int) string { + now := time.Now() + daysAgo := f.rng.Intn(maxDaysAgo) + past := now.AddDate(0, 0, -daysAgo) + return past.Format("2006-01-02") +} + +func (f *Faker) generateFutureDate(maxDaysAhead int) string { + now := time.Now() + daysAhead := f.rng.Intn(maxDaysAhead) + future := now.AddDate(0, 0, daysAhead) + return future.Format("2006-01-02") +} + +func (f *Faker) generatePrice() float64 { + return float64(f.rng.Intn(9999999))/100.0 + 0.50 +} + +func (f *Faker) generateWeight() float64 { + return float64(f.rng.Intn(1000))/10.0 + 0.1 +} + +func (f *Faker) generateStock() int { + return f.rng.Intn(1000) +} + +func (f *Faker) generateCoordinates() (float64, float64) { + lat := float64(f.rng.Intn(900000))/10000.0 + 3.0 + lon := float64(f.rng.Intn(900000))/10000.0 + 33.0 + return lat, lon +} + +func (f *Faker) generateIPAddress() string { + return fmt.Sprintf("10.%d.%d.%d", f.rng.Intn(256), f.rng.Intn(256), f.rng.Intn(256)) +} + +func (f *Faker) generateSessionID() string { + return fmt.Sprintf("sess-%s", f.generateUUID()) +} + +func (f *Faker) generatePassword() string { + const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*" + password := make([]byte, 12) + for i := range password { + password[i] = charset[f.rng.Intn(len(charset))] + } + return string(password) +} + +func (f *Faker) generatePostalCode() string { + return fmt.Sprintf("%04d", f.rng.Intn(9999)) +} + +func (f *Faker) generateHouseNumber() string { + return fmt.Sprintf("House-%d", f.rng.Intn(999)+1) +} + +func (f *Faker) generateAmount() float64 { + return float64(f.rng.Intn(100000))/100.0 + 0.01 +} + +func (f *Faker) pickString(arr []string) string { + return arr[f.rng.Intn(len(arr))] +} + +func (f *Faker) GeneratePerson(fields []string) map[string]any { + firstName := f.pickString(data.EthiopianFirstNames) + lastName := f.pickString(data.EthiopianLastNames) + + result := make(map[string]any) + for _, field := range fields { + switch field { + case "first_name": + result[field] = firstName + case "last_name": + result[field] = lastName + case "full_name": + result[field] = firstName + " " + lastName + case "phone": + result[field] = f.generatePhone() + case "email": + result[field] = f.generateEmail(firstName, lastName) + case "gender": + result[field] = f.pickString(data.Genders) + case "age": + result[field] = f.generateAge() + case "language": + result[field] = f.pickString(data.Languages) + case "city": + result[field] = f.pickString(data.EthiopianCities) + case "region": + result[field] = f.pickString(data.EthiopianRegions) + case "nationality": + result[field] = f.pickString(data.Nationalities) + case "occupation": + result[field] = f.pickString(data.Occupations) + case "company": + result[field] = f.pickString(data.Companies) + case "blood_type": + result[field] = f.pickString(data.BloodTypes) + case "date_of_birth": + result[field] = f.generateDateOfBirth() + case "username": + result[field] = fmt.Sprintf("%s%s%d", firstName, lastName, f.rng.Intn(99)) + case "password": + result[field] = f.generatePassword() + } + } + return result +} + +func (f *Faker) GenerateAddress(fields []string) map[string]any { + result := make(map[string]any) + city := f.pickString(data.EthiopianCities) + + for _, field := range fields { + switch field { + case "street": + result[field] = f.pickString(data.EthiopianStreets) + case "city": + result[field] = city + case "region": + result[field] = f.pickString(data.EthiopianRegions) + case "sub_city": + if subCities, ok := data.EthiopianSubCities[city]; ok && len(subCities) > 0 { + result[field] = f.pickString(subCities) + } else { + result[field] = city + " Sub City" + } + case "postal_code": + result[field] = f.generatePostalCode() + case "zone": + result[field] = "Zone " + fmt.Sprintf("%d", f.rng.Intn(20)+1) + case "woreda": + result[field] = f.pickString(data.EthiopianWoredas) + case "kebele": + result[field] = fmt.Sprintf("Kebele %d", f.rng.Intn(15)+1) + case "house_number": + result[field] = f.generateHouseNumber() + case "latitude", "longitude": + lat, lon := f.generateCoordinates() + result["latitude"] = lat + result["longitude"] = lon + } + } + return result +} + +func (f *Faker) GenerateProduct(fields []string) map[string]any { + result := make(map[string]any) + + for _, field := range fields { + switch field { + case "name": + result[field] = f.pickString(data.ProductNames) + case "category": + result[field] = f.pickString(data.ProductCategories) + case "price": + result[field] = f.generatePrice() + case "sku": + result[field] = f.generateSKU() + case "brand": + result[field] = f.pickString(data.Brands) + case "description": + result[field] = "High quality " + f.pickString(data.ProductCategories) + case "weight": + result[field] = f.generateWeight() + case "stock": + result[field] = f.generateStock() + case "expiry_date": + result[field] = f.generateFutureDate(365*3) + case "manufactured_date": + result[field] = f.generatePastDate(365*2) + } + } + return result +} + +func (f *Faker) GenerateAnalytics(fields []string) map[string]any { + result := make(map[string]any) + + for _, field := range fields { + switch field { + case "user_id": + result[field] = "user-" + fmt.Sprintf("%d", f.rng.Intn(999999)) + case "event": + result[field] = f.pickString(data.EventTypes) + case "timestamp": + now := time.Now() + secondsAgo := f.rng.Int63n(86400 * 30) + t := time.Unix(now.Unix()-secondsAgo, 0) + result[field] = t.Format(time.RFC3339) + case "amount": + result[field] = f.generateAmount() + case "currency": + result[field] = "ETB" + case "session_id": + result[field] = f.generateSessionID() + case "page_url": + result[field] = fmt.Sprintf("https://example.com/page-%d", f.rng.Intn(100)) + case "ip_address": + result[field] = f.generateIPAddress() + case "device_type": + result[field] = f.pickString(data.DeviceTypes) + case "browser": + result[field] = f.pickString(data.Browsers) + case "os": + result[field] = f.pickString(data.OperatingSystems) + case "country": + result[field] = f.pickString(data.Countries) + case "referrer": + result[field] = f.pickString(data.Referrers) + } + } + return result +} + +func (f *Faker) GenerateBatch(count int, dataType types.DataType, fields []string) []map[string]any { + results := make([]map[string]any, count) + + for i := 0; i < count; i++ { + switch dataType { + case types.TypePerson: + results[i] = f.GeneratePerson(fields) + case types.TypeAddress: + results[i] = f.GenerateAddress(fields) + case types.TypeProduct: + results[i] = f.GenerateProduct(fields) + case types.TypeAnalytics: + results[i] = f.GenerateAnalytics(fields) + } + } + + return results +} \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..001f378 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module go-faker + +go 1.22.0 diff --git a/types/types.go b/types/types.go new file mode 100644 index 0000000..363ef4c --- /dev/null +++ b/types/types.go @@ -0,0 +1,120 @@ +package types + +import "time" + +type GenerateRequest struct { + Seed string `json:"seed"` + Count int `json:"count"` + Type DataType `json:"type"` + Fields []string `json:"fields"` +} + +type GenerateResponse struct { + Data []map[string]any `json:"data"` + Meta ResponseMeta `json:"meta"` + Format string `json:"format"` +} + +type ResponseMeta struct { + Count int `json:"count"` + Seed string `json:"seed"` + GeneratedAt string `json:"generated_at"` +} + +type DataType string + +const ( + TypePerson DataType = "person" + TypeAddress DataType = "address" + TypeProduct DataType = "product" + TypeAnalytics DataType = "analytics" +) + +var ValidFields = map[DataType][]string{ + TypePerson: { + "first_name", "last_name", "full_name", + "phone", "email", "gender", "age", + "language", "city", "region", "nationality", + "occupation", "company", "blood_type", + "date_of_birth", "username", "password", + }, + TypeAddress: { + "street", "city", "region", "sub_city", + "postal_code", "zone", "woreda", "kebele", + "house_number", "latitude", "longitude", + }, + TypeProduct: { + "name", "category", "price", "sku", + "brand", "description", "weight", "stock", + "expiry_date", "manufactured_date", + }, + TypeAnalytics: { + "user_id", "event", "timestamp", "amount", + "currency", "session_id", "page_url", + "ip_address", "device_type", "browser", + "os", "country", "referrer", + }, +} + +type Person struct { + FirstName string `json:"first_name,omitempty"` + LastName string `json:"last_name,omitempty"` + FullName string `json:"full_name,omitempty"` + Phone string `json:"phone,omitempty"` + Email string `json:"email,omitempty"` + Gender string `json:"gender,omitempty"` + Age int `json:"age,omitempty"` + Language string `json:"language,omitempty"` + City string `json:"city,omitempty"` + Region string `json:"region,omitempty"` + Nationality string `json:"nationality,omitempty"` + Occupation string `json:"occupation,omitempty"` + Company string `json:"company,omitempty"` + BloodType string `json:"blood_type,omitempty"` + DateOfBirth string `json:"date_of_birth,omitempty"` + Username string `json:"username,omitempty"` + Password string `json:"password,omitempty"` +} + +type Address struct { + Street string `json:"street,omitempty"` + City string `json:"city,omitempty"` + Region string `json:"region,omitempty"` + SubCity string `json:"sub_city,omitempty"` + PostalCode string `json:"postal_code,omitempty"` + Zone string `json:"zone,omitempty"` + Woreda string `json:"woreda,omitempty"` + Kebele string `json:"kebele,omitempty"` + HouseNumber string `json:"house_number,omitempty"` + Latitude float64 `json:"latitude,omitempty"` + Longitude float64 `json:"longitude,omitempty"` +} + +type Product struct { + Name string `json:"name,omitempty"` + Category string `json:"category,omitempty"` + Price float64 `json:"price,omitempty"` + SKU string `json:"sku,omitempty"` + Brand string `json:"brand,omitempty"` + Description string `json:"description,omitempty"` + Weight float64 `json:"weight,omitempty"` + Stock int `json:"stock,omitempty"` + ExpiryDate string `json:"expiry_date,omitempty"` + ManufacturedDate string `json:"manufactured_date,omitempty"` +} + +type Analytics struct { + UserID string `json:"user_id,omitempty"` + Event string `json:"event,omitempty"` + Timestamp time.Time `json:"timestamp,omitempty"` + Amount float64 `json:"amount,omitempty"` + Currency string `json:"currency,omitempty"` + SessionID string `json:"session_id,omitempty"` + PageURL string `json:"page_url,omitempty"` + IPAddress string `json:"ip_address,omitempty"` + DeviceType string `json:"device_type,omitempty"` + Browser string `json:"browser,omitempty"` + OS string `json:"os,omitempty"` + Country string `json:"country,omitempty"` + Referrer string `json:"referrer,omitempty"` +} \ No newline at end of file