diff --git a/.github/workflows/scripts/docker-services.sh b/.github/workflows/scripts/docker-services.sh index 66a105262..1ffbe13c2 100755 --- a/.github/workflows/scripts/docker-services.sh +++ b/.github/workflows/scripts/docker-services.sh @@ -1,15 +1,15 @@ -#!/bin/bash +#!/usr/bin/env bash # # GoFrame Docker Services Manager -# 用于本地开发时管理测试用的Docker服务 +# For managing Docker services used in local development and testing # set -e -# 容器名前缀 +# Container name prefix PREFIX="goframe" -# 颜色定义 +# Color definitions RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' @@ -17,13 +17,13 @@ BLUE='\033[0;34m' CYAN='\033[0;36m' NC='\033[0m' # No Color -# 服务定义 +# Service definitions declare -A SERVICES declare -A SERVICE_PORTS declare -A SERVICE_ENVS declare -A SERVICE_OPTS -# 基础服务 +# Basic services SERVICES["etcd"]="bitnamilegacy/etcd:3.4.24" SERVICE_PORTS["etcd"]="2379:2379" SERVICE_ENVS["etcd"]="-e ALLOW_NONE_AUTHENTICATION=yes" @@ -70,18 +70,18 @@ SERVICE_OPTS["gaussdb"]="--privileged=true" SERVICES["zookeeper"]="zookeeper:3.8" SERVICE_PORTS["zookeeper"]="2181:2181" -# 服务分组 +# Service groups GROUP_DB="mysql mariadb postgres mssql oracle dm gaussdb clickhouse" GROUP_CACHE="redis etcd" GROUP_REGISTRY="polaris zookeeper" GROUP_ALL="etcd redis mysql mariadb postgres mssql clickhouse polaris oracle dm gaussdb zookeeper" -# 工作目录 +# Working directories SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" WORKFLOW_DIR="$PROJECT_ROOT/.github/workflows" -# 打印带颜色的消息 +# Print colored messages print_info() { echo -e "${BLUE}[INFO]${NC} $1" } @@ -98,24 +98,24 @@ print_error() { echo -e "${RED}[ERROR]${NC} $1" } -# 检查Docker是否可用 +# Check if Docker is available check_docker() { if ! command -v docker &> /dev/null; then - print_error "Docker 未安装或不在PATH中" + print_error "Docker is not installed or not in PATH" exit 1 fi if ! docker info &> /dev/null; then - print_error "Docker 服务未运行" + print_error "Docker service is not running" exit 1 fi } -# 获取容器名 +# Get container name get_container_name() { echo "${PREFIX}-$1" } -# 启动单个服务 +# Start a single service start_service() { local service=$1 local container_name=$(get_container_name "$service") @@ -125,39 +125,39 @@ start_service() { local opts="${SERVICE_OPTS[$service]}" if [ -z "$image" ]; then - print_error "未知服务: $service" + print_error "Unknown service: $service" return 1 fi - # 检查容器是否已存在 + # Check if container already exists if docker ps -a --format '{{.Names}}' | grep -q "^${container_name}$"; then if docker ps --format '{{.Names}}' | grep -q "^${container_name}$"; then - print_warning "$service 已在运行" + print_warning "$service is already running" return 0 else - print_info "启动已存在的容器 $service..." + print_info "Starting existing container $service..." docker start "$container_name" > /dev/null - print_success "$service 已启动" + print_success "$service started" return 0 fi fi - print_info "启动 $service..." + print_info "Starting $service..." - # 构建docker run命令 + # Build docker run command local cmd="docker run -d --name $container_name" - # 添加端口映射 + # Add port mappings for port in $ports; do cmd="$cmd -p $port" done - # 添加环境变量 + # Add environment variables if [ -n "$envs" ]; then cmd="$cmd $envs" fi - # 添加其他选项 + # Add other options if [ -n "$opts" ]; then cmd="$cmd $opts" fi @@ -165,42 +165,42 @@ start_service() { cmd="$cmd $image" if eval "$cmd" > /dev/null 2>&1; then - print_success "$service 已启动 (容器: $container_name)" + print_success "$service started (container: $container_name)" else - print_error "$service 启动失败" + print_error "Failed to start $service" return 1 fi } -# 停止单个服务 +# Stop a single service stop_service() { local service=$1 local container_name=$(get_container_name "$service") if docker ps --format '{{.Names}}' | grep -q "^${container_name}$"; then - print_info "停止 $service..." + print_info "Stopping $service..." docker stop "$container_name" > /dev/null - print_success "$service 已停止" + print_success "$service stopped" else - print_warning "$service 未在运行" + print_warning "$service is not running" fi } -# 删除单个服务 +# Remove a single service remove_service() { local service=$1 local container_name=$(get_container_name "$service") if docker ps -a --format '{{.Names}}' | grep -q "^${container_name}$"; then - print_info "删除 $service..." + print_info "Removing $service..." docker rm -f "$container_name" > /dev/null - print_success "$service 已删除" + print_success "$service removed" else - print_warning "$service 容器不存在" + print_warning "$service container does not exist" fi } -# 查看服务日志 +# View service logs logs_service() { local service=$1 local container_name=$(get_container_name "$service") @@ -209,12 +209,12 @@ logs_service() { if docker ps -a --format '{{.Names}}' | grep -q "^${container_name}$"; then docker logs --tail "$lines" -f "$container_name" else - print_error "$service 容器不存在" + print_error "$service container does not exist" return 1 fi } -# 启动docker-compose服务 +# Start docker-compose service start_compose_service() { local service=$1 local compose_file="" @@ -233,22 +233,22 @@ start_compose_service() { compose_file="$WORKFLOW_DIR/consul/docker-compose.yml" ;; *) - print_error "未知compose服务: $service" + print_error "Unknown compose service: $service" return 1 ;; esac if [ -f "$compose_file" ]; then - print_info "启动 $service (docker-compose)..." + print_info "Starting $service (docker-compose)..." docker compose -f "$compose_file" up -d - print_success "$service 已启动" + print_success "$service started" else - print_error "compose文件不存在: $compose_file" + print_error "Compose file does not exist: $compose_file" return 1 fi } -# 停止docker-compose服务 +# Stop docker-compose service stop_compose_service() { local service=$1 local compose_file="" @@ -267,22 +267,22 @@ stop_compose_service() { compose_file="$WORKFLOW_DIR/consul/docker-compose.yml" ;; *) - print_error "未知compose服务: $service" + print_error "Unknown compose service: $service" return 1 ;; esac if [ -f "$compose_file" ]; then - print_info "停止 $service (docker-compose)..." + print_info "Stopping $service (docker-compose)..." docker compose -f "$compose_file" down - print_success "$service 已停止" + print_success "$service stopped" else - print_error "compose文件不存在: $compose_file" + print_error "Compose file does not exist: $compose_file" return 1 fi } -# 显示服务状态 +# Show service status show_status() { echo "" echo -e "${CYAN}========== GoFrame Docker Services Status ==========${NC}" @@ -338,12 +338,12 @@ show_status() { echo "" } -# 显示服务信息 +# Show service information show_service_info() { echo "" echo -e "${CYAN}========== Available Services ==========${NC}" echo "" - echo -e "${YELLOW}基础服务 (独立容器):${NC}" + echo -e "${YELLOW}Basic Services (standalone containers):${NC}" echo "" printf "%-15s %-50s %s\n" "SERVICE" "IMAGE" "PORTS" echo "--------------------------------------------------------------------------------" @@ -353,61 +353,61 @@ show_service_info() { done echo "" - echo -e "${YELLOW}Compose服务 (多容器):${NC}" - echo " apollo - Apollo配置中心 (8080, 8070, 8060, 13306)" - echo " nacos - Nacos注册中心 (8848, 9848, 9555)" - echo " redis-cluster - Redis主从+哨兵集群 (6380-6382, 26379-26381)" - echo " consul - Consul服务发现 (8500, 8600)" + echo -e "${YELLOW}Compose Services (multi-container):${NC}" + echo " apollo - Apollo Config Center (8080, 8070, 8060, 13306)" + echo " nacos - Nacos Registry (8848, 9848, 9555)" + echo " redis-cluster - Redis Primary-Replica + Sentinel Cluster (6380-6382, 26379-26381)" + echo " consul - Consul Service Discovery (8500, 8600)" echo "" - echo -e "${YELLOW}服务分组:${NC}" - echo " db - 数据库: $GROUP_DB" - echo " cache - 缓存: $GROUP_CACHE" - echo " registry - 注册中心: $GROUP_REGISTRY" - echo " all - 所有基础服务" + echo -e "${YELLOW}Service Groups:${NC}" + echo " db - Databases: $GROUP_DB" + echo " cache - Cache: $GROUP_CACHE" + echo " registry - Registry: $GROUP_REGISTRY" + echo " all - All basic services" echo "" } -# 显示帮助 +# Show help show_help() { echo "" echo -e "${CYAN}GoFrame Docker Services Manager${NC}" echo "" - echo "用法: $0 [service|group] [options]" + echo "Usage: $0 [service|group] [options]" echo "" - echo "命令:" - echo " start 启动服务或服务组" - echo " stop 停止服务或服务组" - echo " restart 重启服务或服务组" - echo " remove 删除服务容器" - echo " logs [lines] 查看服务日志 (默认100行)" - echo " status 显示所有服务状态" - echo " info 显示可用服务信息" - echo " clean 删除所有goframe容器" - echo " pull [service] 拉取镜像" + echo "Commands:" + echo " start Start service or service group" + echo " stop Stop service or service group" + echo " restart Restart service or service group" + echo " remove Remove service container" + echo " logs [lines] View service logs (default 100 lines)" + echo " status Show all service status" + echo " info Show available service information" + echo " clean Remove all goframe containers" + echo " pull [service] Pull images" echo "" - echo "服务:" - echo " 基础服务: etcd, redis, mysql, mariadb, postgres, mssql," - echo " clickhouse, polaris, oracle, dm, gaussdb, zookeeper" + echo "Services:" + echo " Basic: etcd, redis, mysql, mariadb, postgres, mssql," + echo " clickhouse, polaris, oracle, dm, gaussdb, zookeeper" echo " Compose: apollo, nacos, redis-cluster, consul" echo "" - echo "服务组:" - echo " db - 所有数据库服务" - echo " cache - 缓存服务 (redis, etcd)" - echo " registry - 注册中心 (polaris, zookeeper)" - echo " all - 所有基础服务" + echo "Service Groups:" + echo " db - All database services" + echo " cache - Cache services (redis, etcd)" + echo " registry - Registry services (polaris, zookeeper)" + echo " all - All basic services" echo "" - echo "示例:" - echo " $0 start mysql # 启动MySQL" - echo " $0 start db # 启动所有数据库" - echo " $0 start all # 启动所有基础服务" - echo " $0 start apollo # 启动Apollo (compose)" - echo " $0 stop all # 停止所有基础服务" - echo " $0 logs mysql 50 # 查看MySQL最近50行日志" - echo " $0 status # 查看服务状态" + echo "Examples:" + echo " $0 start mysql # Start MySQL" + echo " $0 start db # Start all databases" + echo " $0 start all # Start all basic services" + echo " $0 start apollo # Start Apollo (compose)" + echo " $0 stop all # Stop all basic services" + echo " $0 logs mysql 50 # View last 50 lines of MySQL logs" + echo " $0 status # View service status" echo "" } -# 解析服务组 +# Parse service groups parse_services() { local input=$1 case $input in @@ -429,7 +429,7 @@ parse_services() { esac } -# 判断是否为compose服务 +# Check if it's a compose service is_compose_service() { local service=$1 case $service in @@ -442,7 +442,7 @@ is_compose_service() { esac } -# 拉取镜像 +# Pull images pull_images() { local services=$1 @@ -452,28 +452,28 @@ pull_images() { for service in $services; do if [ -n "${SERVICES[$service]}" ]; then - print_info "拉取镜像: ${SERVICES[$service]}" + print_info "Pulling image: ${SERVICES[$service]}" docker pull "${SERVICES[$service]}" fi done } -# 清理所有goframe容器 +# Clean all goframe containers clean_all() { - print_info "删除所有 $PREFIX 容器..." + print_info "Removing all $PREFIX containers..." local containers=$(docker ps -a --filter "name=$PREFIX" --format '{{.Names}}') if [ -n "$containers" ]; then for container in $containers; do docker rm -f "$container" > /dev/null - print_success "已删除: $container" + print_success "Removed: $container" done else - print_info "没有找到 $PREFIX 容器" + print_info "No $PREFIX containers found" fi } -# 获取服务状态标记 +# Get service status mark get_service_status_mark() { local service=$1 local container_name=$(get_container_name "$service") @@ -485,7 +485,7 @@ get_service_status_mark() { fi } -# 获取compose服务状态标记 +# Get compose service status mark get_compose_status_mark() { local service=$1 local running=0 @@ -512,20 +512,20 @@ get_compose_status_mark() { fi } -# 服务选择菜单 +# Service selection menu select_service_menu() { local action=$1 local action_name=$2 echo "" - echo -e "${CYAN}========== 选择${action_name}的服务 ==========${NC}" + echo -e "${CYAN}========== Select Service to ${action_name} ==========${NC}" - # 停止/重启/日志操作时显示运行状态 + # Show running status for stop/restart/logs operations if [[ "$action" == "stop" || "$action" == "restart" || "$action" == "logs" ]]; then - echo -e " (${GREEN}*${NC} 表示正在运行)" + echo -e " (${GREEN}*${NC} indicates running)" fi echo "" - echo -e "${YELLOW}基础服务:${NC}" + echo -e "${YELLOW}Basic Services:${NC}" printf " %b1) etcd %b2) redis %b3) mysql\n" \ "$(get_service_status_mark etcd)" "$(get_service_status_mark redis)" "$(get_service_status_mark mysql)" printf " %b4) mariadb %b5) postgres %b6) mssql\n" \ @@ -535,18 +535,18 @@ select_service_menu() { printf " %b10) dm %b11) gaussdb %b12) zookeeper\n" \ "$(get_service_status_mark dm)" "$(get_service_status_mark gaussdb)" "$(get_service_status_mark zookeeper)" echo "" - echo -e "${YELLOW}Compose服务:${NC}" + echo -e "${YELLOW}Compose Services:${NC}" printf " %b13) apollo %b14) nacos %b15) redis-cluster\n" \ "$(get_compose_status_mark apollo)" "$(get_compose_status_mark nacos)" "$(get_compose_status_mark redis-cluster)" printf " %b16) consul\n" "$(get_compose_status_mark consul)" echo "" - echo -e "${YELLOW}服务组:${NC}" - echo " 17) db (所有数据库) 18) cache (缓存服务)" - echo " 19) registry (注册中心) 20) all (所有基础服务)" + echo -e "${YELLOW}Service Groups:${NC}" + echo " 17) db (all databases) 18) cache (cache services)" + echo " 19) registry (registry services) 20) all (all basic services)" echo "" - echo " 0) 返回上级菜单" + echo " 0) Back to main menu" echo "" - read -p "请选择 [0-20]: " svc_choice + read -p "Select [0-20]: " svc_choice local svc="" case $svc_choice in @@ -572,7 +572,7 @@ select_service_menu() { 20) svc="all" ;; 0) return ;; *) - print_error "无效选择" + print_error "Invalid selection" return ;; esac @@ -614,9 +614,9 @@ select_service_menu() { ;; logs) if is_compose_service "$svc"; then - print_error "Compose服务请使用 docker compose logs 查看" + print_error "For Compose services, please use 'docker compose logs'" else - read -p "显示行数 (默认100): " lines + read -p "Number of lines (default 100): " lines lines=${lines:-100} logs_service "$svc" "$lines" fi @@ -627,40 +627,40 @@ select_service_menu() { esac } -# 交互式菜单 +# Interactive menu interactive_menu() { while true; do echo "" echo -e "${CYAN}========== GoFrame Docker Services Manager ==========${NC}" echo "" - echo " 1) 启动服务" - echo " 2) 停止服务" - echo " 3) 重启服务" - echo " 4) 删除服务" - echo " 5) 查看日志" - echo " 6) 查看状态" - echo " 7) 服务信息" - echo " 8) 清理所有容器" - echo " 9) 拉取镜像" - echo " 0) 退出" + echo " 1) Start Service" + echo " 2) Stop Service" + echo " 3) Restart Service" + echo " 4) Remove Service" + echo " 5) View Logs" + echo " 6) View Status" + echo " 7) Service Info" + echo " 8) Clean All Containers" + echo " 9) Pull Images" + echo " 0) Exit" echo "" - read -p "请选择操作 [0-9]: " choice + read -p "Select operation [0-9]: " choice case $choice in 1) - select_service_menu "start" "启动" + select_service_menu "start" "Start" ;; 2) - select_service_menu "stop" "停止" + select_service_menu "stop" "Stop" ;; 3) - select_service_menu "restart" "重启" + select_service_menu "restart" "Restart" ;; 4) - select_service_menu "remove" "删除" + select_service_menu "remove" "Remove" ;; 5) - select_service_menu "logs" "查看日志" + select_service_menu "logs" "View Logs" ;; 6) show_status @@ -669,26 +669,26 @@ interactive_menu() { show_service_info ;; 8) - read -p "确认删除所有goframe容器? [y/N]: " confirm + read -p "Confirm removing all goframe containers? [y/N]: " confirm if [[ "$confirm" =~ ^[Yy]$ ]]; then clean_all fi ;; 9) - select_service_menu "pull" "拉取镜像" + select_service_menu "pull" "Pull Images" ;; 0) - echo "再见!" + echo "Goodbye!" exit 0 ;; *) - print_error "无效选择" + print_error "Invalid selection" ;; esac done } -# 主函数 +# Main function main() { check_docker @@ -704,7 +704,7 @@ main() { case $command in start) if [ -z "$target" ]; then - print_error "请指定服务名或服务组" + print_error "Please specify service name or service group" exit 1 fi if is_compose_service "$target"; then @@ -717,7 +717,7 @@ main() { ;; stop) if [ -z "$target" ]; then - print_error "请指定服务名或服务组" + print_error "Please specify service name or service group" exit 1 fi if is_compose_service "$target"; then @@ -730,7 +730,7 @@ main() { ;; restart) if [ -z "$target" ]; then - print_error "请指定服务名或服务组" + print_error "Please specify service name or service group" exit 1 fi if is_compose_service "$target"; then @@ -745,7 +745,7 @@ main() { ;; remove|rm) if [ -z "$target" ]; then - print_error "请指定服务名或服务组" + print_error "Please specify service name or service group" exit 1 fi for service in $(parse_services "$target"); do @@ -754,7 +754,7 @@ main() { ;; logs) if [ -z "$target" ]; then - print_error "请指定服务名" + print_error "Please specify service name" exit 1 fi logs_service "$target" "${extra:-100}" @@ -775,7 +775,7 @@ main() { show_help ;; *) - print_error "未知命令: $command" + print_error "Unknown command: $command" show_help exit 1 ;; diff --git a/.gitignore b/.gitignore index 964d5fdd0..be381afce 100644 --- a/.gitignore +++ b/.gitignore @@ -24,4 +24,5 @@ node_modules .docusaurus output .example/ -.golangci.bck.yml \ No newline at end of file +.golangci.bck.yml +*.exe \ No newline at end of file diff --git a/README.MD b/README.MD index 4c6f14391..d06eda44a 100644 --- a/README.MD +++ b/README.MD @@ -45,7 +45,7 @@ go get -u github.com/gogf/gf/v2 💖 [Thanks to all the contributors who made GoFrame possible](https://github.com/gogf/gf/graphs/contributors) 💖 -goframe contributors +goframe contributors ## License diff --git a/cmd/gf/go.mod b/cmd/gf/go.mod index fb4c84e00..1f50688fa 100644 --- a/cmd/gf/go.mod +++ b/cmd/gf/go.mod @@ -3,13 +3,13 @@ module github.com/gogf/gf/cmd/gf/v2 go 1.23.0 require ( - github.com/gogf/gf/contrib/drivers/clickhouse/v2 v2.9.7 - github.com/gogf/gf/contrib/drivers/mssql/v2 v2.9.7 - github.com/gogf/gf/contrib/drivers/mysql/v2 v2.9.7 - github.com/gogf/gf/contrib/drivers/oracle/v2 v2.9.7 - github.com/gogf/gf/contrib/drivers/pgsql/v2 v2.9.7 - github.com/gogf/gf/contrib/drivers/sqlite/v2 v2.9.7 - github.com/gogf/gf/v2 v2.9.7 + github.com/gogf/gf/contrib/drivers/clickhouse/v2 v2.9.8 + github.com/gogf/gf/contrib/drivers/mssql/v2 v2.9.8 + github.com/gogf/gf/contrib/drivers/mysql/v2 v2.9.8 + github.com/gogf/gf/contrib/drivers/oracle/v2 v2.9.8 + github.com/gogf/gf/contrib/drivers/pgsql/v2 v2.9.8 + github.com/gogf/gf/contrib/drivers/sqlite/v2 v2.9.8 + github.com/gogf/gf/v2 v2.9.8 github.com/gogf/selfupdate v0.0.0-20231215043001-5c48c528462f github.com/olekukonko/tablewriter v1.1.0 github.com/schollz/progressbar/v3 v3.15.0 diff --git a/cmd/gf/go.sum b/cmd/gf/go.sum index 3ffe98449..a596009f1 100644 --- a/cmd/gf/go.sum +++ b/cmd/gf/go.sum @@ -46,20 +46,20 @@ github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiU github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI= github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= -github.com/gogf/gf/contrib/drivers/clickhouse/v2 v2.9.7 h1:B/VAGVUTQj45RoEjeZR2OFXmRQqFp5yapQvgYcIHkIo= -github.com/gogf/gf/contrib/drivers/clickhouse/v2 v2.9.7/go.mod h1:MWRkfyj8xRkDMkhGi4OlOip7vpYCc6qltYiX20RUWZE= -github.com/gogf/gf/contrib/drivers/mssql/v2 v2.9.7 h1:qJcpAhJ+4UXhDdClb3Uobl+1efZjzva42DCQTfvRiBU= -github.com/gogf/gf/contrib/drivers/mssql/v2 v2.9.7/go.mod h1:+jbjp4GV6/zVjup8t3wDlSyuMN8n16DWXt6G7gUW3ic= -github.com/gogf/gf/contrib/drivers/mysql/v2 v2.9.7 h1:fY8b8CDRaG0o6+Va1pn5cxcrLCaOdvOnuCwEvFx0xf0= -github.com/gogf/gf/contrib/drivers/mysql/v2 v2.9.7/go.mod h1:mAdAYnp/e1O3ftP4PZH3QZl64OO2CSX2/P/1aru8udg= -github.com/gogf/gf/contrib/drivers/oracle/v2 v2.9.7 h1:DaZahL4VK7yHV4oSh6eObNYoH/qJyI0635ptOdWl/Gk= -github.com/gogf/gf/contrib/drivers/oracle/v2 v2.9.7/go.mod h1:X3Sfef066Xfm6JJUw59U7YPIzYqBSMD5VcuTq09Bbag= -github.com/gogf/gf/contrib/drivers/pgsql/v2 v2.9.7 h1:hfwnVbqnNrIDDmIrju7ukgNYwoTYUQd61NiB7CemC3g= -github.com/gogf/gf/contrib/drivers/pgsql/v2 v2.9.7/go.mod h1:mRfoQHOLVzzPaIpM64i9fjk1iIp/HY5LOySYJO+ziE0= -github.com/gogf/gf/contrib/drivers/sqlite/v2 v2.9.7 h1:mzs0MblNT0pOlUB00c/hTcAenQ5N/cB651wh9VCJitc= -github.com/gogf/gf/contrib/drivers/sqlite/v2 v2.9.7/go.mod h1:Z2MgGyag0fZQ2+9ykafD7tQhau9h5ie3Chg/GUzfy5E= -github.com/gogf/gf/v2 v2.9.7 h1:Vp3VGZ7drPs89tZslT6j6BKBTaw7Xs3DMGWx4MlVtMA= -github.com/gogf/gf/v2 v2.9.7/go.mod h1:Svl1N+E8G/QshU2DUbh/3J/AJauqCgUnxHurXWR4Qx0= +github.com/gogf/gf/contrib/drivers/clickhouse/v2 v2.9.8 h1:L72OB2HPuZSHtJ2ipBzI+62rGGDRdwYjequ1v+zctpg= +github.com/gogf/gf/contrib/drivers/clickhouse/v2 v2.9.8/go.mod h1:D0UySg70Bd264F5AScYmz1Hl8vjzlUJ7YvqBJc5OFbo= +github.com/gogf/gf/contrib/drivers/mssql/v2 v2.9.8 h1:DT5zHfo9/VkbJ+TF7kUasvv4dbU5uctoj+JGbrzgdYE= +github.com/gogf/gf/contrib/drivers/mssql/v2 v2.9.8/go.mod h1:cDd91Zd8LxFF+xxOflRRqw0WTTCpAJ0nf0KKRA+nvTE= +github.com/gogf/gf/contrib/drivers/mysql/v2 v2.9.8 h1:XZ4Ya/50xpjf81+4genr33iJXR2dxJmqYKxGyXlLRqA= +github.com/gogf/gf/contrib/drivers/mysql/v2 v2.9.8/go.mod h1:wtm2NJb/L3CbDOmyUc7TsOpWHTCMakg1QRG7B/oKrRs= +github.com/gogf/gf/contrib/drivers/oracle/v2 v2.9.8 h1:ZrqABJsUnhNDz8VAem1XXONBTywl6r+GHQH05i+4W1g= +github.com/gogf/gf/contrib/drivers/oracle/v2 v2.9.8/go.mod h1:YTFyeVk2Rgu/JMUhFxkjYzWaBc+yZ6wAvY54XVZoNko= +github.com/gogf/gf/contrib/drivers/pgsql/v2 v2.9.8 h1:Dc227FD1uf9nNBPFEjMEgIoAJbAgeYeNrOrjviDgPzY= +github.com/gogf/gf/contrib/drivers/pgsql/v2 v2.9.8/go.mod h1:o3EpB4Ti3+x/axzRMJg2k7TrLiWZiSTxP0v64LBkk5k= +github.com/gogf/gf/contrib/drivers/sqlite/v2 v2.9.8 h1:LHEhzsBfIo8xHvOUuLDQW1q7Qix1vnBabH/iivCRghs= +github.com/gogf/gf/contrib/drivers/sqlite/v2 v2.9.8/go.mod h1:SX6dRONaJGafzCoMIrn8CkRM4fIvtmJRt/aYclUHy3Q= +github.com/gogf/gf/v2 v2.9.8 h1:El0HwksTzeRk0DQV4Lh7S9DbsIwKInhHSHGcH7qJumM= +github.com/gogf/gf/v2 v2.9.8/go.mod h1:Svl1N+E8G/QshU2DUbh/3J/AJauqCgUnxHurXWR4Qx0= github.com/gogf/selfupdate v0.0.0-20231215043001-5c48c528462f h1:7xfXR/BhG3JDqO1s45n65Oyx9t4E/UqDOXep6jXdLCM= github.com/gogf/selfupdate v0.0.0-20231215043001-5c48c528462f/go.mod h1:HnYoio6S7VaFJdryKcD/r9HgX+4QzYfr00XiXUo/xz0= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= diff --git a/cmd/gf/internal/cmd/cmd_env.go b/cmd/gf/internal/cmd/cmd_env.go index 8c48be62e..52153bade 100644 --- a/cmd/gf/internal/cmd/cmd_env.go +++ b/cmd/gf/internal/cmd/cmd_env.go @@ -37,11 +37,13 @@ type cEnvInput struct { type cEnvOutput struct{} func (c cEnv) Index(ctx context.Context, in cEnvInput) (out *cEnvOutput, err error) { - result, err := gproc.ShellExec(ctx, "go env") - if err != nil { - mlog.Fatal(err) - } + result, execErr := gproc.ShellExec(ctx, "go env") + // Note: go env may return non-zero exit code when there are warnings (e.g., invalid characters in env vars), + // but it still outputs valid environment variables. So we only fail if result is empty. if result == "" { + if execErr != nil { + mlog.Fatal(execErr) + } mlog.Fatal(`retrieving Golang environment variables failed, did you install Golang?`) } var ( @@ -59,7 +61,9 @@ func (c cEnv) Index(ctx context.Context, in cEnvInput) (out *cEnvOutput, err err } match, _ := gregex.MatchString(`(.+?)=(.*)`, line) if len(match) < 3 { - mlog.Fatalf(`invalid Golang environment variable: "%s"`, line) + // Skip lines that don't match key=value format (e.g., warning messages from go env) + mlog.Debugf(`invalid Golang environment variable: "%s"`, line) + continue } array = append(array, []string{gstr.Trim(match[1]), gstr.Trim(match[2])}) } diff --git a/cmd/gf/internal/cmd/cmd_z_init_test.go b/cmd/gf/internal/cmd/cmd_z_init_test.go index 1e71cbd8b..d066a2599 100644 --- a/cmd/gf/internal/cmd/cmd_z_init_test.go +++ b/cmd/gf/internal/cmd/cmd_z_init_test.go @@ -15,9 +15,11 @@ import ( ) var ( - ctx = context.Background() - testDB gdb.DB - link = "mysql:root:12345678@tcp(127.0.0.1:3306)/test?loc=Local&parseTime=true" + ctx = context.Background() + testDB gdb.DB + testPgDB gdb.DB + link = "mysql:root:12345678@tcp(127.0.0.1:3306)/test?loc=Local&parseTime=true" + linkPg = "pgsql:postgres:12345678@tcp(127.0.0.1:5432)/test" ) func init() { @@ -28,6 +30,10 @@ func init() { if err != nil { panic(err) } + // PostgreSQL connection (optional, may not be available in all environments) + testPgDB, _ = gdb.New(gdb.ConfigNode{ + Link: linkPg, + }) } func dropTableWithDb(db gdb.DB, table string) { @@ -36,3 +42,11 @@ func dropTableWithDb(db gdb.DB, table string) { gtest.Error(err) } } + +// dropTableStd uses standard SQL syntax compatible with MySQL and PostgreSQL. +func dropTableStd(db gdb.DB, table string) { + dropTableStmt := fmt.Sprintf("DROP TABLE IF EXISTS %s", table) + if _, err := db.Exec(ctx, dropTableStmt); err != nil { + gtest.Error(err) + } +} diff --git a/cmd/gf/internal/cmd/cmd_z_unit_env_test.go b/cmd/gf/internal/cmd/cmd_z_unit_env_test.go new file mode 100644 index 000000000..cecb67edc --- /dev/null +++ b/cmd/gf/internal/cmd/cmd_z_unit_env_test.go @@ -0,0 +1,84 @@ +// Copyright GoFrame gf Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package cmd + +import ( + "testing" + + "github.com/gogf/gf/v2/test/gtest" + "github.com/gogf/gf/v2/text/gregex" + "github.com/gogf/gf/v2/text/gstr" +) + +func Test_Env_Index(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + // Test that env command runs without error + _, err := Env.Index(ctx, cEnvInput{}) + t.AssertNil(err) + }) +} + +func Test_Env_ParseGoEnvOutput(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + // Test parsing normal go env output + lines := []string{ + "set GOPATH=C:\\Users\\test\\go", + "set GOROOT=C:\\Go", + "set GOOS=windows", + "GOARCH=amd64", // Unix format without "set " prefix + "CGO_ENABLED=0", + } + + for _, line := range lines { + line = gstr.Trim(line) + if gstr.Pos(line, "set ") == 0 { + line = line[4:] + } + match, _ := gregex.MatchString(`(.+?)=(.*)`, line) + t.Assert(len(match) >= 3, true) + } + }) +} + +func Test_Env_ParseGoEnvOutput_WithWarnings(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + // Test parsing go env output that contains warning messages + // These lines should be skipped without causing errors + lines := []string{ + "go: stripping unprintable or unescapable characters from %\"GOPROXY\"%", + "go: warning: some warning message", + "# this is a comment", + "", + "set GOPATH=C:\\Users\\test\\go", + "set GOOS=windows", + } + + array := make([][]string, 0) + for _, line := range lines { + line = gstr.Trim(line) + if line == "" { + continue + } + if gstr.Pos(line, "set ") == 0 { + line = line[4:] + } + match, _ := gregex.MatchString(`(.+?)=(.*)`, line) + if len(match) < 3 { + // Skip lines that don't match key=value format (e.g., warning messages) + continue + } + array = append(array, []string{gstr.Trim(match[1]), gstr.Trim(match[2])}) + } + + // Should have parsed 2 valid environment variables + t.Assert(len(array), 2) + t.Assert(array[0][0], "GOPATH") + t.Assert(array[0][1], "C:\\Users\\test\\go") + t.Assert(array[1][0], "GOOS") + t.Assert(array[1][1], "windows") + }) +} diff --git a/cmd/gf/internal/cmd/cmd_z_unit_fix_test.go b/cmd/gf/internal/cmd/cmd_z_unit_fix_test.go index ad53e4129..bd456f786 100644 --- a/cmd/gf/internal/cmd/cmd_z_unit_fix_test.go +++ b/cmd/gf/internal/cmd/cmd_z_unit_fix_test.go @@ -10,6 +10,7 @@ import ( "testing" "github.com/gogf/gf/v2/test/gtest" + "github.com/gogf/gf/v2/text/gstr" ) func Test_Fix_doFixV25Content(t *testing.T) { @@ -22,3 +23,82 @@ func Test_Fix_doFixV25Content(t *testing.T) { t.AssertNil(err) }) } + +func Test_Fix_doFixV25Content_WithReplacement(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + var ( + f = cFix{} + content = `s.BindHookHandlerByMap("/path", map[string]ghttp.HandlerFunc{ + ghttp.HookBeforeServe: func(r *ghttp.Request) {}, + })` + ) + newContent, err := f.doFixV25Content(content) + t.AssertNil(err) + // Verify the replacement was made + t.Assert(gstr.Contains(newContent, "map[ghttp.HookName]ghttp.HandlerFunc"), true) + t.Assert(gstr.Contains(newContent, "map[string]ghttp.HandlerFunc"), false) + }) +} + +func Test_Fix_doFixV25Content_NoMatch(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + var ( + f = cFix{} + content = `package main + +func main() { + fmt.Println("Hello World") +} +` + ) + newContent, err := f.doFixV25Content(content) + t.AssertNil(err) + // Content should remain unchanged + t.Assert(newContent, content) + }) +} + +func Test_Fix_doFixV25Content_MultipleMatches(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + var ( + f = cFix{} + content = ` +s.BindHookHandlerByMap("/path1", map[string]ghttp.HandlerFunc{}) +s.BindHookHandlerByMap("/path2", map[string]ghttp.HandlerFunc{}) +` + ) + newContent, err := f.doFixV25Content(content) + t.AssertNil(err) + // Both should be replaced + count := gstr.Count(newContent, "map[ghttp.HookName]ghttp.HandlerFunc") + t.Assert(count, 2) + }) +} + +func Test_Fix_doFixV25Content_EmptyContent(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + var ( + f = cFix{} + content = "" + ) + newContent, err := f.doFixV25Content(content) + t.AssertNil(err) + t.Assert(newContent, "") + }) +} + +func Test_Fix_doFixV25Content_ComplexPath(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + var ( + f = cFix{} + content = `s.BindHookHandlerByMap("/api/v1/user/{id}/profile", map[string]ghttp.HandlerFunc{ + ghttp.HookBeforeServe: func(r *ghttp.Request) { + r.Response.Write("before") + }, + })` + ) + newContent, err := f.doFixV25Content(content) + t.AssertNil(err) + t.Assert(gstr.Contains(newContent, "map[ghttp.HookName]ghttp.HandlerFunc"), true) + }) +} diff --git a/cmd/gf/internal/cmd/cmd_z_unit_gen_dao_issue_test.go b/cmd/gf/internal/cmd/cmd_z_unit_gen_dao_issue_test.go index 02d5f5936..3a8421fda 100644 --- a/cmd/gf/internal/cmd/cmd_z_unit_gen_dao_issue_test.go +++ b/cmd/gf/internal/cmd/cmd_z_unit_gen_dao_issue_test.go @@ -460,3 +460,398 @@ func Test_Gen_Dao_Issue3749(t *testing.T) { } }) } + +// https://github.com/gogf/gf/issues/4629 +// Test tables pattern matching with * wildcard. +func Test_Gen_Dao_Issue4629_TablesPattern_Star(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + var ( + err error + db = testDB + table1 = "trade_order" + table2 = "trade_item" + table3 = "user_info" + table4 = "user_log" + table5 = "config" + sqlFilePath = gtest.DataPath(`gendao`, `tables_pattern.sql`) + ) + dropTableStd(db, table1) + dropTableStd(db, table2) + dropTableStd(db, table3) + dropTableStd(db, table4) + dropTableStd(db, table5) + t.AssertNil(execSqlFile(db, sqlFilePath)) + defer dropTableStd(db, table1) + defer dropTableStd(db, table2) + defer dropTableStd(db, table3) + defer dropTableStd(db, table4) + defer dropTableStd(db, table5) + + var ( + path = gfile.Temp(guid.S()) + group = "test" + in = gendao.CGenDaoInput{ + Path: path, + Link: link, + Group: group, + Tables: "trade_*", // Should match trade_order, trade_item + } + ) + err = gutil.FillStructWithDefault(&in) + t.AssertNil(err) + + err = gfile.Mkdir(path) + t.AssertNil(err) + + pwd := gfile.Pwd() + err = gfile.Chdir(path) + t.AssertNil(err) + defer gfile.Chdir(pwd) + defer gfile.RemoveAll(path) + + _, err = gendao.CGenDao{}.Dao(ctx, in) + t.AssertNil(err) + + // Should generate 2 dao files: trade_order.go, trade_item.go + generatedFiles, err := gfile.ScanDir(gfile.Join(path, "dao"), "*.go", false) + t.AssertNil(err) + t.Assert(len(generatedFiles), 2) + + // Verify the correct files are generated + t.Assert(gfile.Exists(gfile.Join(path, "dao", "trade_order.go")), true) + t.Assert(gfile.Exists(gfile.Join(path, "dao", "trade_item.go")), true) + // user_* and config should NOT be generated + t.Assert(gfile.Exists(gfile.Join(path, "dao", "user_info.go")), false) + t.Assert(gfile.Exists(gfile.Join(path, "dao", "user_log.go")), false) + t.Assert(gfile.Exists(gfile.Join(path, "dao", "config.go")), false) + }) +} + +// https://github.com/gogf/gf/issues/4629 +// Test tables pattern matching with multiple patterns. +func Test_Gen_Dao_Issue4629_TablesPattern_Multiple(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + var ( + err error + db = testDB + table1 = "trade_order" + table2 = "trade_item" + table3 = "user_info" + table4 = "user_log" + table5 = "config" + sqlFilePath = gtest.DataPath(`gendao`, `tables_pattern.sql`) + ) + dropTableStd(db, table1) + dropTableStd(db, table2) + dropTableStd(db, table3) + dropTableStd(db, table4) + dropTableStd(db, table5) + t.AssertNil(execSqlFile(db, sqlFilePath)) + defer dropTableStd(db, table1) + defer dropTableStd(db, table2) + defer dropTableStd(db, table3) + defer dropTableStd(db, table4) + defer dropTableStd(db, table5) + + var ( + path = gfile.Temp(guid.S()) + group = "test" + in = gendao.CGenDaoInput{ + Path: path, + Link: link, + Group: group, + Tables: "trade_*,user_*", // Should match trade_order, trade_item, user_info, user_log + } + ) + err = gutil.FillStructWithDefault(&in) + t.AssertNil(err) + + err = gfile.Mkdir(path) + t.AssertNil(err) + + pwd := gfile.Pwd() + err = gfile.Chdir(path) + t.AssertNil(err) + defer gfile.Chdir(pwd) + defer gfile.RemoveAll(path) + + _, err = gendao.CGenDao{}.Dao(ctx, in) + t.AssertNil(err) + + // Should generate 4 dao files + generatedFiles, err := gfile.ScanDir(gfile.Join(path, "dao"), "*.go", false) + t.AssertNil(err) + t.Assert(len(generatedFiles), 4) + + // Verify the correct files are generated + t.Assert(gfile.Exists(gfile.Join(path, "dao", "trade_order.go")), true) + t.Assert(gfile.Exists(gfile.Join(path, "dao", "trade_item.go")), true) + t.Assert(gfile.Exists(gfile.Join(path, "dao", "user_info.go")), true) + t.Assert(gfile.Exists(gfile.Join(path, "dao", "user_log.go")), true) + // config should NOT be generated + t.Assert(gfile.Exists(gfile.Join(path, "dao", "config.go")), false) + }) +} + +// https://github.com/gogf/gf/issues/4629 +// Test tables pattern mixed with exact table name. +func Test_Gen_Dao_Issue4629_TablesPattern_Mixed(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + var ( + err error + db = testDB + table1 = "trade_order" + table2 = "trade_item" + table3 = "user_info" + table4 = "user_log" + table5 = "config" + sqlFilePath = gtest.DataPath(`gendao`, `tables_pattern.sql`) + ) + dropTableStd(db, table1) + dropTableStd(db, table2) + dropTableStd(db, table3) + dropTableStd(db, table4) + dropTableStd(db, table5) + t.AssertNil(execSqlFile(db, sqlFilePath)) + defer dropTableStd(db, table1) + defer dropTableStd(db, table2) + defer dropTableStd(db, table3) + defer dropTableStd(db, table4) + defer dropTableStd(db, table5) + + var ( + path = gfile.Temp(guid.S()) + group = "test" + in = gendao.CGenDaoInput{ + Path: path, + Link: link, + Group: group, + Tables: "trade_*,config", // Pattern + exact name + } + ) + err = gutil.FillStructWithDefault(&in) + t.AssertNil(err) + + err = gfile.Mkdir(path) + t.AssertNil(err) + + pwd := gfile.Pwd() + err = gfile.Chdir(path) + t.AssertNil(err) + defer gfile.Chdir(pwd) + defer gfile.RemoveAll(path) + + _, err = gendao.CGenDao{}.Dao(ctx, in) + t.AssertNil(err) + + // Should generate 3 dao files: trade_order.go, trade_item.go, config.go + generatedFiles, err := gfile.ScanDir(gfile.Join(path, "dao"), "*.go", false) + t.AssertNil(err) + t.Assert(len(generatedFiles), 3) + + // Verify the correct files are generated + t.Assert(gfile.Exists(gfile.Join(path, "dao", "trade_order.go")), true) + t.Assert(gfile.Exists(gfile.Join(path, "dao", "trade_item.go")), true) + t.Assert(gfile.Exists(gfile.Join(path, "dao", "config.go")), true) + // user_* should NOT be generated + t.Assert(gfile.Exists(gfile.Join(path, "dao", "user_info.go")), false) + t.Assert(gfile.Exists(gfile.Join(path, "dao", "user_log.go")), false) + }) +} + +// https://github.com/gogf/gf/issues/4629 +// Test tables pattern with ? wildcard (single character match). +func Test_Gen_Dao_Issue4629_TablesPattern_Question(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + var ( + err error + db = testDB + table1 = "trade_order" + table2 = "trade_item" + table3 = "user_info" + table4 = "user_log" + table5 = "config" + sqlFilePath = gtest.DataPath(`gendao`, `tables_pattern.sql`) + ) + dropTableStd(db, table1) + dropTableStd(db, table2) + dropTableStd(db, table3) + dropTableStd(db, table4) + dropTableStd(db, table5) + t.AssertNil(execSqlFile(db, sqlFilePath)) + defer dropTableStd(db, table1) + defer dropTableStd(db, table2) + defer dropTableStd(db, table3) + defer dropTableStd(db, table4) + defer dropTableStd(db, table5) + + var ( + path = gfile.Temp(guid.S()) + group = "test" + in = gendao.CGenDaoInput{ + Path: path, + Link: link, + Group: group, + Tables: "user_???", // ? matches single char: user_log (3 chars) but not user_info (4 chars) + } + ) + err = gutil.FillStructWithDefault(&in) + t.AssertNil(err) + + err = gfile.Mkdir(path) + t.AssertNil(err) + + pwd := gfile.Pwd() + err = gfile.Chdir(path) + t.AssertNil(err) + defer gfile.Chdir(pwd) + defer gfile.RemoveAll(path) + + _, err = gendao.CGenDao{}.Dao(ctx, in) + t.AssertNil(err) + + // Should generate 1 dao file: user_log.go (3 chars after user_) + generatedFiles, err := gfile.ScanDir(gfile.Join(path, "dao"), "*.go", false) + t.AssertNil(err) + t.Assert(len(generatedFiles), 1) + + // Verify only user_log is generated + t.Assert(gfile.Exists(gfile.Join(path, "dao", "user_log.go")), true) + t.Assert(gfile.Exists(gfile.Join(path, "dao", "user_info.go")), false) // 4 chars, doesn't match + }) +} + +// https://github.com/gogf/gf/issues/4629 +// Test that exact table names still work (backward compatibility). +func Test_Gen_Dao_Issue4629_TablesPattern_ExactNames(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + var ( + err error + db = testDB + table1 = "trade_order" + table2 = "trade_item" + table3 = "user_info" + table4 = "user_log" + table5 = "config" + sqlFilePath = gtest.DataPath(`gendao`, `tables_pattern.sql`) + ) + dropTableStd(db, table1) + dropTableStd(db, table2) + dropTableStd(db, table3) + dropTableStd(db, table4) + dropTableStd(db, table5) + t.AssertNil(execSqlFile(db, sqlFilePath)) + defer dropTableStd(db, table1) + defer dropTableStd(db, table2) + defer dropTableStd(db, table3) + defer dropTableStd(db, table4) + defer dropTableStd(db, table5) + + var ( + path = gfile.Temp(guid.S()) + group = "test" + in = gendao.CGenDaoInput{ + Path: path, + Link: link, + Group: group, + Tables: "trade_order,config", // Exact names, no patterns + } + ) + err = gutil.FillStructWithDefault(&in) + t.AssertNil(err) + + err = gfile.Mkdir(path) + t.AssertNil(err) + + pwd := gfile.Pwd() + err = gfile.Chdir(path) + t.AssertNil(err) + defer gfile.Chdir(pwd) + defer gfile.RemoveAll(path) + + _, err = gendao.CGenDao{}.Dao(ctx, in) + t.AssertNil(err) + + // Should generate 2 dao files + generatedFiles, err := gfile.ScanDir(gfile.Join(path, "dao"), "*.go", false) + t.AssertNil(err) + t.Assert(len(generatedFiles), 2) + + // Verify exactly the specified tables are generated + t.Assert(gfile.Exists(gfile.Join(path, "dao", "trade_order.go")), true) + t.Assert(gfile.Exists(gfile.Join(path, "dao", "config.go")), true) + t.Assert(gfile.Exists(gfile.Join(path, "dao", "trade_item.go")), false) + }) +} + +// https://github.com/gogf/gf/issues/4629 +// Test tables pattern matching with PostgreSQL. +func Test_Gen_Dao_Issue4629_TablesPattern_PgSql(t *testing.T) { + if testPgDB == nil { + t.Skip("PostgreSQL database not available, skipping test") + return + } + gtest.C(t, func(t *gtest.T) { + var ( + err error + db = testPgDB + table1 = "trade_order" + table2 = "trade_item" + table3 = "user_info" + table4 = "user_log" + table5 = "config" + sqlFilePath = gtest.DataPath(`gendao`, `tables_pattern.sql`) + ) + dropTableStd(db, table1) + dropTableStd(db, table2) + dropTableStd(db, table3) + dropTableStd(db, table4) + dropTableStd(db, table5) + t.AssertNil(execSqlFile(db, sqlFilePath)) + defer dropTableStd(db, table1) + defer dropTableStd(db, table2) + defer dropTableStd(db, table3) + defer dropTableStd(db, table4) + defer dropTableStd(db, table5) + + // Test tables pattern with tablesEx pattern + var ( + path = gfile.Temp(guid.S()) + group = "test" + in = gendao.CGenDaoInput{ + Path: path, + Link: linkPg, + Group: group, + Tables: "trade_*,user_*,config", // Match only our test tables + TablesEx: "user_*", // Exclude user_* tables + } + ) + err = gutil.FillStructWithDefault(&in) + t.AssertNil(err) + + err = gfile.Mkdir(path) + t.AssertNil(err) + + pwd := gfile.Pwd() + err = gfile.Chdir(path) + t.AssertNil(err) + defer gfile.Chdir(pwd) + defer gfile.RemoveAll(path) + + _, err = gendao.CGenDao{}.Dao(ctx, in) + t.AssertNil(err) + + // Should generate 3 dao files: trade_order, trade_item, config (user_* excluded by tablesEx) + generatedFiles, err := gfile.ScanDir(gfile.Join(path, "dao"), "*.go", false) + t.AssertNil(err) + t.Assert(len(generatedFiles), 3) + + // Verify the correct files are generated + t.Assert(gfile.Exists(gfile.Join(path, "dao", "trade_order.go")), true) + t.Assert(gfile.Exists(gfile.Join(path, "dao", "trade_item.go")), true) + t.Assert(gfile.Exists(gfile.Join(path, "dao", "config.go")), true) + // user_* should NOT be generated (excluded by tablesEx) + t.Assert(gfile.Exists(gfile.Join(path, "dao", "user_info.go")), false) + t.Assert(gfile.Exists(gfile.Join(path, "dao", "user_log.go")), false) + }) +} diff --git a/cmd/gf/internal/cmd/cmd_z_unit_gen_dao_sharding_test.go b/cmd/gf/internal/cmd/cmd_z_unit_gen_dao_sharding_test.go index 5a3989667..df2a7a45d 100644 --- a/cmd/gf/internal/cmd/cmd_z_unit_gen_dao_sharding_test.go +++ b/cmd/gf/internal/cmd/cmd_z_unit_gen_dao_sharding_test.go @@ -18,6 +18,92 @@ import ( "github.com/gogf/gf/cmd/gf/v2/internal/cmd/gendao" ) +// Test_Gen_Dao_Sharding_Overlapping tests the fix for issue #4603. +// When sharding patterns have overlapping prefixes (like "a_?", "a_b_?", "a_c_?"), +// longer (more specific) patterns should be matched first. +// https://github.com/gogf/gf/issues/4603 +func Test_Gen_Dao_Sharding_Overlapping(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + var ( + err error + db = testDB + tableA1 = "a_1" + tableA2 = "a_2" + tableAB1 = "a_b_1" + tableAB2 = "a_b_2" + tableAC1 = "a_c_1" + tableAC2 = "a_c_2" + sqlFilePath = gtest.DataPath(`gendao`, `sharding`, `sharding_overlapping.sql`) + ) + dropTableWithDb(db, tableA1) + dropTableWithDb(db, tableA2) + dropTableWithDb(db, tableAB1) + dropTableWithDb(db, tableAB2) + dropTableWithDb(db, tableAC1) + dropTableWithDb(db, tableAC2) + t.AssertNil(execSqlFile(db, sqlFilePath)) + defer dropTableWithDb(db, tableA1) + defer dropTableWithDb(db, tableA2) + defer dropTableWithDb(db, tableAB1) + defer dropTableWithDb(db, tableAB2) + defer dropTableWithDb(db, tableAC1) + defer dropTableWithDb(db, tableAC2) + + var ( + path = gfile.Temp(guid.S()) + group = "test" + in = gendao.CGenDaoInput{ + Path: path, + Link: link, + Group: group, + Prefix: "", + // Patterns with overlapping prefixes - order should not matter due to sorting fix + ShardingPattern: []string{ + `a_?`, // shortest, matches a_1, a_2 but also a_b_1, a_c_1 without fix + `a_b_?`, // longer, should match a_b_1, a_b_2 + `a_c_?`, // longer, should match a_c_1, a_c_2 + }, + } + ) + err = gutil.FillStructWithDefault(&in) + t.AssertNil(err) + + err = gfile.Mkdir(path) + t.AssertNil(err) + + pwd := gfile.Pwd() + err = gfile.Chdir(path) + t.AssertNil(err) + defer gfile.Chdir(pwd) + defer gfile.RemoveAll(path) + + _, err = gendao.CGenDao{}.Dao(ctx, in) + t.AssertNil(err) + + // Should generate 3 dao files: a.go, a_b.go, a_c.go (plus internal versions) + generatedFiles, err := gfile.ScanDir(path, "*.go", true) + t.AssertNil(err) + // 3 sharding groups * 4 files each (dao, internal, do, entity) = 12 files + t.Assert(len(generatedFiles), 12) + + var ( + daoAContent = gfile.GetContents(gfile.Join(path, "dao", "a.go")) + daoABContent = gfile.GetContents(gfile.Join(path, "dao", "a_b.go")) + daoACContent = gfile.GetContents(gfile.Join(path, "dao", "a_c.go")) + ) + + // Verify each sharding group has correct dao file generated + t.Assert(gstr.Contains(daoAContent, "aShardingHandler"), true) + t.Assert(gstr.Contains(daoAContent, "m.Sharding(gdb.ShardingConfig{"), true) + + t.Assert(gstr.Contains(daoABContent, "aBShardingHandler"), true) + t.Assert(gstr.Contains(daoABContent, "m.Sharding(gdb.ShardingConfig{"), true) + + t.Assert(gstr.Contains(daoACContent, "aCShardingHandler"), true) + t.Assert(gstr.Contains(daoACContent, "m.Sharding(gdb.ShardingConfig{"), true) + }) +} + func Test_Gen_Dao_Sharding(t *testing.T) { gtest.C(t, func(t *gtest.T) { var ( diff --git a/cmd/gf/internal/cmd/cmd_z_unit_gen_enums_test.go b/cmd/gf/internal/cmd/cmd_z_unit_gen_enums_test.go new file mode 100644 index 000000000..1cee880a7 --- /dev/null +++ b/cmd/gf/internal/cmd/cmd_z_unit_gen_enums_test.go @@ -0,0 +1,158 @@ +// Copyright GoFrame gf Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package cmd + +import ( + "path/filepath" + "testing" + + "github.com/gogf/gf/v2/os/gfile" + "github.com/gogf/gf/v2/test/gtest" + "github.com/gogf/gf/v2/util/guid" + "github.com/gogf/gf/v2/util/gutil" + + "github.com/gogf/gf/cmd/gf/v2/internal/cmd/genenums" +) + +// https://github.com/gogf/gf/issues/4387 +// Test that the output path is relative to the original working directory, +// not the source directory after Chdir. +func Test_Gen_Enums_Issue4387_RelativePath(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + var ( + // Create temp directory to simulate user's project + tempPath = gfile.Temp(guid.S()) + // Copy testdata to temp directory + srcTestData = gtest.DataPath("issue", "4387") + ) + + // Setup: create temp project structure + err := gfile.CopyDir(srcTestData, tempPath) + t.AssertNil(err) + defer gfile.Remove(tempPath) + + // Save original working directory + originalWd := gfile.Pwd() + + // Change to temp directory (simulate user being in project root) + err = gfile.Chdir(tempPath) + t.AssertNil(err) + defer gfile.Chdir(originalWd) // Restore original working directory + + // Run gen enums with relative paths + var ( + srcFolder = "api" + outputPath = filepath.FromSlash("internal/packed/packed_enums.go") + in = genenums.CGenEnumsInput{ + Src: srcFolder, + Path: outputPath, + } + ) + err = gutil.FillStructWithDefault(&in) + t.AssertNil(err) + + _, err = genenums.CGenEnums{}.Enums(ctx, in) + t.AssertNil(err) + + // Expected: file should be created at tempPath/internal/packed/packed_enums.go + expectedPath := filepath.Join(tempPath, "internal", "packed", "packed_enums.go") + // Bug: file is created at tempPath/api/internal/packed/packed_enums.go + wrongPath := filepath.Join(tempPath, "api", "internal", "packed", "packed_enums.go") + + // Assert the file is at the expected location + t.Assert(gfile.Exists(expectedPath), true) + // Assert the file is NOT at the wrong location + t.Assert(gfile.Exists(wrongPath), false) + }) +} + +// Test gen enums with absolute output path (should work correctly) +func Test_Gen_Enums_AbsolutePath(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + var ( + tempPath = gfile.Temp(guid.S()) + srcTestData = gtest.DataPath("issue", "4387") + ) + + err := gfile.CopyDir(srcTestData, tempPath) + t.AssertNil(err) + defer gfile.Remove(tempPath) + + originalWd := gfile.Pwd() + err = gfile.Chdir(tempPath) + t.AssertNil(err) + defer gfile.Chdir(originalWd) + + // Use absolute path for output + var ( + srcFolder = "api" + outputPath = filepath.Join(tempPath, "internal", "packed", "packed_enums.go") + in = genenums.CGenEnumsInput{ + Src: srcFolder, + Path: outputPath, + } + ) + err = gutil.FillStructWithDefault(&in) + t.AssertNil(err) + + _, err = genenums.CGenEnums{}.Enums(ctx, in) + t.AssertNil(err) + + // Assert the file exists at absolute path + t.Assert(gfile.Exists(outputPath), true) + }) +} + +// Test gen enums in monorepo mode (cd app/xxx/ then run command) +func Test_Gen_Enums_Issue4387_Monorepo(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + var ( + // Simulate monorepo structure + tempPath = gfile.Temp(guid.S()) + srcTestData = gtest.DataPath("issue", "4387") + // app/myapp is the subdirectory in monorepo + appPath = filepath.Join(tempPath, "app", "myapp") + ) + + // Create monorepo structure: tempPath/app/myapp/api/... + err := gfile.Mkdir(appPath) + t.AssertNil(err) + // Copy testdata into app/myapp + err = gfile.CopyDir(srcTestData, appPath) + t.AssertNil(err) + defer gfile.Remove(tempPath) + + originalWd := gfile.Pwd() + + // cd app/myapp (simulate user in monorepo subdirectory) + err = gfile.Chdir(appPath) + t.AssertNil(err) + defer gfile.Chdir(originalWd) + + var ( + srcFolder = "api" + outputPath = filepath.FromSlash("internal/packed/packed_enums.go") + in = genenums.CGenEnumsInput{ + Src: srcFolder, + Path: outputPath, + } + ) + err = gutil.FillStructWithDefault(&in) + t.AssertNil(err) + + _, err = genenums.CGenEnums{}.Enums(ctx, in) + t.AssertNil(err) + + // Expected: file at app/myapp/internal/packed/packed_enums.go + expectedPath := filepath.Join(appPath, "internal", "packed", "packed_enums.go") + // Bug: file at app/myapp/api/internal/packed/packed_enums.go + wrongPath := filepath.Join(appPath, "api", "internal", "packed", "packed_enums.go") + + t.Assert(gfile.Exists(expectedPath), true) + t.Assert(gfile.Exists(wrongPath), false) + }) +} diff --git a/cmd/gf/internal/cmd/cmd_z_unit_gen_pb_test.go b/cmd/gf/internal/cmd/cmd_z_unit_gen_pb_test.go index 80cf1e814..6c3cff236 100644 --- a/cmd/gf/internal/cmd/cmd_z_unit_gen_pb_test.go +++ b/cmd/gf/internal/cmd/cmd_z_unit_gen_pb_test.go @@ -88,3 +88,76 @@ func TestGenPbIssue3953(t *testing.T) { t.Assert(gstr.Contains(genContent, notExceptText), false) }) } + +func TestGenPb_MultipleTags(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + var ( + outputPath = gfile.Temp(guid.S()) + outputApiPath = filepath.Join(outputPath, "api") + outputCtrlPath = filepath.Join(outputPath, "controller") + + protobufFolder = gtest.DataPath("genpb") + in = genpb.CGenPbInput{ + Path: protobufFolder, + OutputApi: outputApiPath, + OutputCtrl: outputCtrlPath, + } + err error + ) + err = gfile.Mkdir(outputApiPath) + t.AssertNil(err) + err = gfile.Mkdir(outputCtrlPath) + t.AssertNil(err) + defer gfile.Remove(outputPath) + + _, err = genpb.CGenPb{}.Pb(ctx, in) + t.AssertNil(err) + + // Test multiple_tags.proto output + genContent := gfile.GetContents(filepath.Join(outputApiPath, "multiple_tags.pb.go")) + // Id field should have combined validation tags: v:"required#Id > 0" + t.Assert(gstr.Contains(genContent, `v:"required#Id > 0"`), true) + // Name field should have dc tag from plain comment + t.Assert(gstr.Contains(genContent, `dc:"User name for login"`), true) + // Email field should have combined validation and dc tag + t.Assert(gstr.Contains(genContent, `v:"requiredemail"`), true) + t.Assert(gstr.Contains(genContent, `dc:"User email address"`), true) + }) +} + +func TestGenPb_NestedMessage(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + var ( + outputPath = gfile.Temp(guid.S()) + outputApiPath = filepath.Join(outputPath, "api") + outputCtrlPath = filepath.Join(outputPath, "controller") + + protobufFolder = gtest.DataPath("genpb") + in = genpb.CGenPbInput{ + Path: protobufFolder, + OutputApi: outputApiPath, + OutputCtrl: outputCtrlPath, + } + err error + ) + err = gfile.Mkdir(outputApiPath) + t.AssertNil(err) + err = gfile.Mkdir(outputCtrlPath) + t.AssertNil(err) + defer gfile.Remove(outputPath) + + _, err = genpb.CGenPb{}.Pb(ctx, in) + t.AssertNil(err) + + // Test nested_message.proto output + genContent := gfile.GetContents(filepath.Join(outputApiPath, "nested_message.pb.go")) + // Order.OrderId should have v:"required" + t.Assert(gstr.Contains(genContent, `v:"required"`), true) + // Order.Detail should have dc:"Order details" + t.Assert(gstr.Contains(genContent, `dc:"Order details"`), true) + // OrderDetail.Quantity should have v:"min:1" + t.Assert(gstr.Contains(genContent, `v:"min:1"`), true) + // OrderDetail.Price should have v:"min:0.01" + t.Assert(gstr.Contains(genContent, `v:"min:0.01"`), true) + }) +} diff --git a/cmd/gf/internal/cmd/cmd_z_unit_gen_service_test.go b/cmd/gf/internal/cmd/cmd_z_unit_gen_service_test.go index 40ee7158e..322667dbc 100644 --- a/cmd/gf/internal/cmd/cmd_z_unit_gen_service_test.go +++ b/cmd/gf/internal/cmd/cmd_z_unit_gen_service_test.go @@ -156,3 +156,85 @@ func Test_Issue3835(t *testing.T) { t.Assert(gfile.GetContents(genFile), gfile.GetContents(expectFile)) }) } + +func Test_Gen_Service_CamelCase(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + var ( + path = gfile.Temp(guid.S()) + dstFolder = path + filepath.FromSlash("/service") + srvFolder = gtest.DataPath("genservice", "logic") + in = genservice.CGenServiceInput{ + SrcFolder: srvFolder, + DstFolder: dstFolder, + DstFileNameCase: "Camel", + WatchFile: "", + StPattern: "", + Packages: nil, + ImportPrefix: "", + Clear: false, + } + ) + err := gutil.FillStructWithDefault(&in) + t.AssertNil(err) + + err = gfile.Mkdir(path) + t.AssertNil(err) + defer gfile.Remove(path) + + // Clean up generated logic.go + genSrv := srvFolder + filepath.FromSlash("/logic.go") + defer gfile.Remove(genSrv) + + _, err = genservice.CGenService{}.Service(ctx, in) + t.AssertNil(err) + + // Files should be in CamelCase + files, err := gfile.ScanDir(dstFolder, "*.go", true) + t.AssertNil(err) + t.Assert(files, []string{ + dstFolder + filepath.FromSlash("/Article.go"), + dstFolder + filepath.FromSlash("/Base.go"), + dstFolder + filepath.FromSlash("/Delivery.go"), + dstFolder + filepath.FromSlash("/User.go"), + }) + }) +} + +func Test_Gen_Service_PackagesFilter(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + var ( + path = gfile.Temp(guid.S()) + dstFolder = path + filepath.FromSlash("/service") + srvFolder = gtest.DataPath("genservice", "logic") + in = genservice.CGenServiceInput{ + SrcFolder: srvFolder, + DstFolder: dstFolder, + DstFileNameCase: "Snake", + WatchFile: "", + StPattern: "", + Packages: []string{"user"}, + ImportPrefix: "", + Clear: false, + } + ) + err := gutil.FillStructWithDefault(&in) + t.AssertNil(err) + + err = gfile.Mkdir(path) + t.AssertNil(err) + defer gfile.Remove(path) + + // Clean up generated logic.go + genSrv := srvFolder + filepath.FromSlash("/logic.go") + defer gfile.Remove(genSrv) + + _, err = genservice.CGenService{}.Service(ctx, in) + t.AssertNil(err) + + // Only user.go should be generated + files, err := gfile.ScanDir(dstFolder, "*.go", true) + t.AssertNil(err) + t.Assert(len(files), 1) + t.Assert(files[0], dstFolder+filepath.FromSlash("/user.go")) + }) +} diff --git a/cmd/gf/internal/cmd/cmd_z_unit_pack_test.go b/cmd/gf/internal/cmd/cmd_z_unit_pack_test.go new file mode 100644 index 000000000..ac39e8da0 --- /dev/null +++ b/cmd/gf/internal/cmd/cmd_z_unit_pack_test.go @@ -0,0 +1,346 @@ +// Copyright GoFrame gf Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package cmd + +import ( + "context" + "path/filepath" + "testing" + + "github.com/gogf/gf/v2/os/gfile" + "github.com/gogf/gf/v2/test/gtest" + "github.com/gogf/gf/v2/text/gstr" + "github.com/gogf/gf/v2/util/guid" +) + +func Test_Pack_ToGoFile(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + var ( + srcPath = gfile.Temp(guid.S()) + dstPath = gfile.Temp(guid.S()) + dstFile = filepath.Join(dstPath, "packed", "data.go") + ) + // Create source directory with test files + err := gfile.Mkdir(srcPath) + t.AssertNil(err) + defer gfile.Remove(srcPath) + + err = gfile.Mkdir(dstPath) + t.AssertNil(err) + defer gfile.Remove(dstPath) + + // Create test files + err = gfile.PutContents(filepath.Join(srcPath, "test.txt"), "hello world") + t.AssertNil(err) + err = gfile.PutContents(filepath.Join(srcPath, "test.json"), `{"key":"value"}`) + t.AssertNil(err) + + // Create packed directory + err = gfile.Mkdir(filepath.Join(dstPath, "packed")) + t.AssertNil(err) + + // Pack to go file + _, err = Pack.Index(context.Background(), cPackInput{ + Src: srcPath, + Dst: dstFile, + Name: "packed", + }) + t.AssertNil(err) + + // Verify output file exists + t.Assert(gfile.Exists(dstFile), true) + + // Verify it's a valid Go file + content := gfile.GetContents(dstFile) + t.Assert(gstr.Contains(content, "package packed"), true) + t.Assert(gstr.Contains(content, "func init()"), true) + }) +} + +func Test_Pack_ToBinaryFile(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + var ( + srcPath = gfile.Temp(guid.S()) + dstPath = gfile.Temp(guid.S()) + dstFile = filepath.Join(dstPath, "data.bin") + ) + // Create source directory with test files + err := gfile.Mkdir(srcPath) + t.AssertNil(err) + defer gfile.Remove(srcPath) + + err = gfile.Mkdir(dstPath) + t.AssertNil(err) + defer gfile.Remove(dstPath) + + // Create test file + err = gfile.PutContents(filepath.Join(srcPath, "test.txt"), "binary content") + t.AssertNil(err) + + // Pack to binary file (no Name specified) + _, err = Pack.Index(context.Background(), cPackInput{ + Src: srcPath, + Dst: dstFile, + }) + t.AssertNil(err) + + // Verify output file exists + t.Assert(gfile.Exists(dstFile), true) + + // Verify it's a binary file (not a Go file) + content := gfile.GetContents(dstFile) + t.Assert(gstr.Contains(content, "package"), false) + }) +} + +func Test_Pack_MultipleSources(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + var ( + srcPath1 = gfile.Temp(guid.S()) + srcPath2 = gfile.Temp(guid.S()) + dstPath = gfile.Temp(guid.S()) + dstFile = filepath.Join(dstPath, "packed", "multi.go") + ) + // Create source directories + err := gfile.Mkdir(srcPath1) + t.AssertNil(err) + defer gfile.Remove(srcPath1) + + err = gfile.Mkdir(srcPath2) + t.AssertNil(err) + defer gfile.Remove(srcPath2) + + err = gfile.Mkdir(dstPath) + t.AssertNil(err) + defer gfile.Remove(dstPath) + + // Create test files in each source + err = gfile.PutContents(filepath.Join(srcPath1, "file1.txt"), "content1") + t.AssertNil(err) + err = gfile.PutContents(filepath.Join(srcPath2, "file2.txt"), "content2") + t.AssertNil(err) + + // Create packed directory + err = gfile.Mkdir(filepath.Join(dstPath, "packed")) + t.AssertNil(err) + + // Pack multiple sources (comma-separated) + _, err = Pack.Index(context.Background(), cPackInput{ + Src: srcPath1 + "," + srcPath2, + Dst: dstFile, + Name: "packed", + }) + t.AssertNil(err) + + // Verify output file exists + t.Assert(gfile.Exists(dstFile), true) + }) +} + +func Test_Pack_WithPrefix(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + var ( + srcPath = gfile.Temp(guid.S()) + dstPath = gfile.Temp(guid.S()) + dstFile = filepath.Join(dstPath, "packed", "prefix.go") + ) + // Create source directory + err := gfile.Mkdir(srcPath) + t.AssertNil(err) + defer gfile.Remove(srcPath) + + err = gfile.Mkdir(dstPath) + t.AssertNil(err) + defer gfile.Remove(dstPath) + + // Create test file + err = gfile.PutContents(filepath.Join(srcPath, "test.txt"), "with prefix") + t.AssertNil(err) + + // Create packed directory + err = gfile.Mkdir(filepath.Join(dstPath, "packed")) + t.AssertNil(err) + + // Pack with prefix + _, err = Pack.Index(context.Background(), cPackInput{ + Src: srcPath, + Dst: dstFile, + Name: "packed", + Prefix: "/static", + }) + t.AssertNil(err) + + // Verify output file exists + t.Assert(gfile.Exists(dstFile), true) + }) +} + +func Test_Pack_WithKeepPath(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + var ( + srcPath = gfile.Temp(guid.S()) + dstPath = gfile.Temp(guid.S()) + dstFile = filepath.Join(dstPath, "packed", "keeppath.go") + ) + // Create source directory with subdirectory + err := gfile.Mkdir(srcPath) + t.AssertNil(err) + defer gfile.Remove(srcPath) + + err = gfile.Mkdir(dstPath) + t.AssertNil(err) + defer gfile.Remove(dstPath) + + // Create subdirectory and file + subDir := filepath.Join(srcPath, "subdir") + err = gfile.Mkdir(subDir) + t.AssertNil(err) + err = gfile.PutContents(filepath.Join(subDir, "test.txt"), "keeppath content") + t.AssertNil(err) + + // Create packed directory + err = gfile.Mkdir(filepath.Join(dstPath, "packed")) + t.AssertNil(err) + + // Pack with keepPath + _, err = Pack.Index(context.Background(), cPackInput{ + Src: srcPath, + Dst: dstFile, + Name: "packed", + KeepPath: true, + }) + t.AssertNil(err) + + // Verify output file exists + t.Assert(gfile.Exists(dstFile), true) + }) +} + +func Test_Pack_AutoPackageName(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + var ( + srcPath = gfile.Temp(guid.S()) + dstPath = gfile.Temp(guid.S()) + dstFile = filepath.Join(dstPath, "mypackage", "data.go") + ) + // Create source directory + err := gfile.Mkdir(srcPath) + t.AssertNil(err) + defer gfile.Remove(srcPath) + + err = gfile.Mkdir(dstPath) + t.AssertNil(err) + defer gfile.Remove(dstPath) + + // Create test file + err = gfile.PutContents(filepath.Join(srcPath, "test.txt"), "auto package name") + t.AssertNil(err) + + // Create mypackage directory + err = gfile.Mkdir(filepath.Join(dstPath, "mypackage")) + t.AssertNil(err) + + // Pack without Name - should use directory name "mypackage" + _, err = Pack.Index(context.Background(), cPackInput{ + Src: srcPath, + Dst: dstFile, + // Name not specified, should be auto-detected as "mypackage" + }) + t.AssertNil(err) + + // Verify output file exists and has correct package name + t.Assert(gfile.Exists(dstFile), true) + content := gfile.GetContents(dstFile) + t.Assert(gstr.Contains(content, "package mypackage"), true) + }) +} + +func Test_Pack_EmptySource(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + var ( + srcPath = gfile.Temp(guid.S()) + dstPath = gfile.Temp(guid.S()) + dstFile = filepath.Join(dstPath, "packed", "empty.go") + ) + // Create empty source directory + err := gfile.Mkdir(srcPath) + t.AssertNil(err) + defer gfile.Remove(srcPath) + + err = gfile.Mkdir(dstPath) + t.AssertNil(err) + defer gfile.Remove(dstPath) + + // Create packed directory + err = gfile.Mkdir(filepath.Join(dstPath, "packed")) + t.AssertNil(err) + + // Pack empty directory + _, err = Pack.Index(context.Background(), cPackInput{ + Src: srcPath, + Dst: dstFile, + Name: "packed", + }) + t.AssertNil(err) + + // Verify output file exists (even if source is empty) + t.Assert(gfile.Exists(dstFile), true) + }) +} + +func Test_Pack_NestedDirectories(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + var ( + srcPath = gfile.Temp(guid.S()) + dstPath = gfile.Temp(guid.S()) + dstFile = filepath.Join(dstPath, "packed", "nested.go") + ) + // Create source directory with nested structure + err := gfile.Mkdir(srcPath) + t.AssertNil(err) + defer gfile.Remove(srcPath) + + err = gfile.Mkdir(dstPath) + t.AssertNil(err) + defer gfile.Remove(dstPath) + + // Create nested directories and files + level1 := filepath.Join(srcPath, "level1") + level2 := filepath.Join(level1, "level2") + level3 := filepath.Join(level2, "level3") + err = gfile.Mkdir(level3) + t.AssertNil(err) + + err = gfile.PutContents(filepath.Join(srcPath, "root.txt"), "root") + t.AssertNil(err) + err = gfile.PutContents(filepath.Join(level1, "l1.txt"), "level1") + t.AssertNil(err) + err = gfile.PutContents(filepath.Join(level2, "l2.txt"), "level2") + t.AssertNil(err) + err = gfile.PutContents(filepath.Join(level3, "l3.txt"), "level3") + t.AssertNil(err) + + // Create packed directory + err = gfile.Mkdir(filepath.Join(dstPath, "packed")) + t.AssertNil(err) + + // Pack nested directories + _, err = Pack.Index(context.Background(), cPackInput{ + Src: srcPath, + Dst: dstFile, + Name: "packed", + }) + t.AssertNil(err) + + // Verify output file exists + t.Assert(gfile.Exists(dstFile), true) + + // Verify content includes all files + content := gfile.GetContents(dstFile) + t.Assert(gstr.Contains(content, "package packed"), true) + }) +} diff --git a/cmd/gf/internal/cmd/gendao/gendao.go b/cmd/gf/internal/cmd/gendao/gendao.go index bc6ad943a..056d91dcc 100644 --- a/cmd/gf/internal/cmd/gendao/gendao.go +++ b/cmd/gf/internal/cmd/gendao/gendao.go @@ -9,6 +9,7 @@ package gendao import ( "context" "fmt" + "sort" "strings" "github.com/olekukonko/tablewriter" @@ -187,7 +188,27 @@ func doGenDaoForArray(ctx context.Context, index int, in CGenDaoInput) { var tableNames []string if in.Tables != "" { - tableNames = gstr.SplitAndTrim(in.Tables, ",") + inputTables := gstr.SplitAndTrim(in.Tables, ",") + // Check if any table pattern contains wildcard characters. + // https://github.com/gogf/gf/issues/4629 + var hasPattern bool + for _, t := range inputTables { + if containsWildcard(t) { + hasPattern = true + break + } + } + if hasPattern { + // Fetch all tables first, then filter by patterns. + allTables, err := db.Tables(context.TODO()) + if err != nil { + mlog.Fatalf("fetching tables failed: %+v", err) + } + tableNames = filterTablesByPatterns(allTables, inputTables) + } else { + // Use exact table names as before. + tableNames = inputTables + } } else { tableNames, err = db.Tables(context.TODO()) if err != nil { @@ -198,22 +219,11 @@ func doGenDaoForArray(ctx context.Context, index int, in CGenDaoInput) { if in.TablesEx != "" { array := garray.NewStrArrayFrom(tableNames) for _, p := range gstr.SplitAndTrim(in.TablesEx, ",") { - if gstr.Contains(p, "*") || gstr.Contains(p, "?") { - p = gstr.ReplaceByMap(p, map[string]string{ - "\r": "", - "\n": "", - }) - p = gstr.ReplaceByMap(p, map[string]string{ - "*": "\r", - "?": "\n", - }) - p = gregex.Quote(p) - p = gstr.ReplaceByMap(p, map[string]string{ - "\r": ".*", - "\n": ".", - }) + if containsWildcard(p) { + // Use exact match with ^ and $ anchors for consistency with tables pattern. + regPattern := "^" + patternToRegex(p) + "$" for _, v := range array.Clone().Slice() { - if gregex.IsMatchString(p, v) { + if gregex.IsMatchString(regPattern, v) { array.RemoveValue(v) } } @@ -240,13 +250,22 @@ func doGenDaoForArray(ctx context.Context, index int, in CGenDaoInput) { newTableNames = make([]string, len(tableNames)) shardingNewTableSet = gset.NewStrSet() ) + // Sort sharding patterns by length descending, so that longer (more specific) patterns + // are matched first. This prevents shorter patterns like "a_?" from incorrectly matching + // tables that should match longer patterns like "a_b_?" or "a_c_?". + // https://github.com/gogf/gf/issues/4603 + sortedShardingPatterns := make([]string, len(in.ShardingPattern)) + copy(sortedShardingPatterns, in.ShardingPattern) + sort.Slice(sortedShardingPatterns, func(i, j int) bool { + return len(sortedShardingPatterns[i]) > len(sortedShardingPatterns[j]) + }) for i, tableName := range tableNames { newTableName := tableName for _, v := range removePrefixArray { newTableName = gstr.TrimLeftStr(newTableName, v, 1) } - if len(in.ShardingPattern) > 0 { - for _, pattern := range in.ShardingPattern { + if len(sortedShardingPatterns) > 0 { + for _, pattern := range sortedShardingPatterns { var ( match []string regPattern = gstr.Replace(pattern, "?", `(.+)`) @@ -262,10 +281,11 @@ func doGenDaoForArray(ctx context.Context, index int, in CGenDaoInput) { newTableName = gstr.Trim(newTableName, `_.-`) if shardingNewTableSet.Contains(newTableName) { tableNames[i] = "" - continue + break } // Add prefix to sharding table name, if not, the isSharding check would not match. shardingNewTableSet.Add(in.Prefix + newTableName) + break } } newTableName = in.Prefix + newTableName @@ -411,3 +431,61 @@ func getTemplateFromPathOrDefault(filePath string, def string) string { } return def } + +// containsWildcard checks if the pattern contains wildcard characters (* or ?). +func containsWildcard(pattern string) bool { + return gstr.Contains(pattern, "*") || gstr.Contains(pattern, "?") +} + +// patternToRegex converts a wildcard pattern to a regex pattern. +// Wildcard characters: * matches any characters, ? matches single character. +func patternToRegex(pattern string) string { + pattern = gstr.ReplaceByMap(pattern, map[string]string{ + "\r": "", + "\n": "", + }) + pattern = gstr.ReplaceByMap(pattern, map[string]string{ + "*": "\r", + "?": "\n", + }) + pattern = gregex.Quote(pattern) + pattern = gstr.ReplaceByMap(pattern, map[string]string{ + "\r": ".*", + "\n": ".", + }) + return pattern +} + +// filterTablesByPatterns filters tables by given patterns. +// Patterns support wildcard characters: * matches any characters, ? matches single character. +// https://github.com/gogf/gf/issues/4629 +func filterTablesByPatterns(allTables []string, patterns []string) []string { + var result []string + matched := make(map[string]bool) + allTablesSet := make(map[string]bool) + for _, t := range allTables { + allTablesSet[t] = true + } + for _, p := range patterns { + if containsWildcard(p) { + regPattern := "^" + patternToRegex(p) + "$" + for _, table := range allTables { + if !matched[table] && gregex.IsMatchString(regPattern, table) { + result = append(result, table) + matched[table] = true + } + } + } else { + // Exact table name, use direct string comparison. + if !allTablesSet[p] { + mlog.Printf(`table "%s" does not exist, skipped`, p) + continue + } + if !matched[p] { + result = append(result, p) + matched[p] = true + } + } + } + return result +} diff --git a/cmd/gf/internal/cmd/gendao/gendao_test.go b/cmd/gf/internal/cmd/gendao/gendao_test.go new file mode 100644 index 000000000..80b67d907 --- /dev/null +++ b/cmd/gf/internal/cmd/gendao/gendao_test.go @@ -0,0 +1,182 @@ +// Copyright GoFrame gf Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package gendao + +import ( + "testing" + + "github.com/gogf/gf/v2/test/gtest" +) + +// Test containsWildcard function. +func Test_containsWildcard(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + t.Assert(containsWildcard("trade_*"), true) + t.Assert(containsWildcard("user_?"), true) + t.Assert(containsWildcard("*"), true) + t.Assert(containsWildcard("?"), true) + t.Assert(containsWildcard("trade_order"), false) + t.Assert(containsWildcard(""), false) + }) +} + +// Test patternToRegex function. +func Test_patternToRegex(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + // * should become .* + t.Assert(patternToRegex("trade_*"), "trade_.*") + // ? should become . + t.Assert(patternToRegex("user_???"), "user_...") + // Mixed + t.Assert(patternToRegex("*_order_?"), ".*_order_.") + // No wildcards - should escape special regex chars + t.Assert(patternToRegex("trade_order"), "trade_order") + // Just * + t.Assert(patternToRegex("*"), ".*") + }) +} + +// Test filterTablesByPatterns with * wildcard. +func Test_filterTablesByPatterns_Star(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + allTables := []string{"trade_order", "trade_item", "user_info", "user_log", "config"} + + // Single pattern with * + result := filterTablesByPatterns(allTables, []string{"trade_*"}) + t.Assert(len(result), 2) + t.AssertIN("trade_order", result) + t.AssertIN("trade_item", result) + + // Multiple patterns with * + result = filterTablesByPatterns(allTables, []string{"trade_*", "user_*"}) + t.Assert(len(result), 4) + t.AssertIN("trade_order", result) + t.AssertIN("trade_item", result) + t.AssertIN("user_info", result) + t.AssertIN("user_log", result) + }) +} + +// Test filterTablesByPatterns with ? wildcard. +func Test_filterTablesByPatterns_Question(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + allTables := []string{"trade_order", "trade_item", "user_info", "user_log", "config"} + + // ? matches single character: user_log (3 chars) but not user_info (4 chars) + result := filterTablesByPatterns(allTables, []string{"user_???"}) + t.Assert(len(result), 1) + t.AssertIN("user_log", result) + t.AssertNI("user_info", result) + + // user_???? should match user_info (4 chars) + result = filterTablesByPatterns(allTables, []string{"user_????"}) + t.Assert(len(result), 1) + t.AssertIN("user_info", result) + t.AssertNI("user_log", result) + }) +} + +// Test filterTablesByPatterns with mixed patterns and exact names. +func Test_filterTablesByPatterns_Mixed(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + allTables := []string{"trade_order", "trade_item", "user_info", "user_log", "config"} + + // Pattern + exact name + result := filterTablesByPatterns(allTables, []string{"trade_*", "config"}) + t.Assert(len(result), 3) + t.AssertIN("trade_order", result) + t.AssertIN("trade_item", result) + t.AssertIN("config", result) + t.AssertNI("user_info", result) + t.AssertNI("user_log", result) + }) +} + +// Test filterTablesByPatterns with exact names only (backward compatibility). +func Test_filterTablesByPatterns_ExactNames(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + allTables := []string{"trade_order", "trade_item", "user_info", "user_log", "config"} + + // Exact names only + result := filterTablesByPatterns(allTables, []string{"trade_order", "config"}) + t.Assert(len(result), 2) + t.AssertIN("trade_order", result) + t.AssertIN("config", result) + t.AssertNI("trade_item", result) + }) +} + +// Test filterTablesByPatterns - no duplicates when table matches multiple patterns. +func Test_filterTablesByPatterns_NoDuplicates(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + allTables := []string{"trade_order", "trade_item", "user_info"} + + // trade_order matches both patterns, should only appear once + result := filterTablesByPatterns(allTables, []string{"trade_*", "trade_order"}) + t.Assert(len(result), 2) // trade_order, trade_item + + // Count occurrences of trade_order + count := 0 + for _, v := range result { + if v == "trade_order" { + count++ + } + } + t.Assert(count, 1) // No duplicates + }) +} + +// Test filterTablesByPatterns - pattern matches nothing. +func Test_filterTablesByPatterns_NoMatch(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + allTables := []string{"trade_order", "trade_item", "user_info"} + + // Pattern that matches nothing + result := filterTablesByPatterns(allTables, []string{"nonexistent_*"}) + t.Assert(len(result), 0) + }) +} + +// Test filterTablesByPatterns - empty input. +func Test_filterTablesByPatterns_Empty(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + allTables := []string{"trade_order", "trade_item"} + + // Empty patterns + result := filterTablesByPatterns(allTables, []string{}) + t.Assert(len(result), 0) + + // Empty tables + result = filterTablesByPatterns([]string{}, []string{"trade_*"}) + t.Assert(len(result), 0) + }) +} + +// Test filterTablesByPatterns - "*" matches all tables. +func Test_filterTablesByPatterns_MatchAll(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + allTables := []string{"trade_order", "trade_item", "user_info", "user_log", "config"} + + // "*" should match all tables + result := filterTablesByPatterns(allTables, []string{"*"}) + t.Assert(len(result), 5) + }) +} + +// Test filterTablesByPatterns - non-existent exact table name should be skipped. +func Test_filterTablesByPatterns_NonExistent(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + allTables := []string{"trade_order", "trade_item", "user_info"} + + // Mix of existing and non-existing tables + result := filterTablesByPatterns(allTables, []string{"trade_order", "nonexistent", "user_info"}) + t.Assert(len(result), 2) + t.AssertIN("trade_order", result) + t.AssertIN("user_info", result) + t.AssertNI("nonexistent", result) + }) +} diff --git a/cmd/gf/internal/cmd/genenums/genenums.go b/cmd/gf/internal/cmd/genenums/genenums.go index a214072c2..27ff362e8 100644 --- a/cmd/gf/internal/cmd/genenums/genenums.go +++ b/cmd/gf/internal/cmd/genenums/genenums.go @@ -55,6 +55,13 @@ func (c CGenEnums) Enums(ctx context.Context, in CGenEnumsInput) (out *CGenEnums if realPath == "" { mlog.Fatalf(`source folder path "%s" does not exist`, in.Src) } + // Convert output path to absolute before Chdir, so it remains correct after directory change. + // See: https://github.com/gogf/gf/issues/4387 + outputPath := gfile.Abs(in.Path) + + originPwd := gfile.Pwd() + defer gfile.Chdir(originPwd) + err = gfile.Chdir(realPath) if err != nil { mlog.Fatal(err) @@ -72,14 +79,14 @@ func (c CGenEnums) Enums(ctx context.Context, in CGenEnumsInput) (out *CGenEnums p := NewEnumsParser(in.Prefixes) p.ParsePackages(pkgs) var enumsContent = gstr.ReplaceByMap(consts.TemplateGenEnums, g.MapStrStr{ - "{PackageName}": gfile.Basename(gfile.Dir(in.Path)), + "{PackageName}": gfile.Basename(gfile.Dir(outputPath)), "{EnumsJson}": "`" + p.Export() + "`", }) enumsContent = gstr.Trim(enumsContent) - if err = gfile.PutContents(in.Path, enumsContent); err != nil { + if err = gfile.PutContents(outputPath, enumsContent); err != nil { return } - mlog.Printf(`generated enums go file: %s`, in.Path) + mlog.Printf(`generated enums go file: %s`, outputPath) mlog.Print("done!") return } diff --git a/cmd/gf/internal/cmd/genenums/genenums_z_unit_test.go b/cmd/gf/internal/cmd/genenums/genenums_z_unit_test.go new file mode 100644 index 000000000..d3a3a58fc --- /dev/null +++ b/cmd/gf/internal/cmd/genenums/genenums_z_unit_test.go @@ -0,0 +1,368 @@ +// Copyright GoFrame gf Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package genenums + +import ( + "go/constant" + "path/filepath" + "testing" + + "golang.org/x/tools/go/packages" + + "github.com/gogf/gf/v2/encoding/gjson" + "github.com/gogf/gf/v2/os/gfile" + "github.com/gogf/gf/v2/test/gtest" + "github.com/gogf/gf/v2/util/guid" +) + +func Test_NewEnumsParser(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + // Test creating parser without prefixes + p := NewEnumsParser(nil) + t.AssertNE(p, nil) + t.Assert(len(p.enums), 0) + t.Assert(len(p.prefixes), 0) + t.AssertNE(p.parsedPkg, nil) + t.AssertNE(p.standardPackages, nil) + }) +} + +func Test_NewEnumsParser_WithPrefixes(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + // Test creating parser with prefixes + prefixes := []string{"github.com/gogf", "github.com/test"} + p := NewEnumsParser(prefixes) + t.AssertNE(p, nil) + t.Assert(len(p.prefixes), 2) + t.Assert(p.prefixes[0], "github.com/gogf") + t.Assert(p.prefixes[1], "github.com/test") + }) +} + +func Test_EnumsParser_Export_Empty(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + // Test exporting empty enums + p := NewEnumsParser(nil) + result := p.Export() + t.Assert(result, "{}") + }) +} + +func Test_EnumsParser_Export_WithEnums(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + // Test exporting with manually added enums + p := NewEnumsParser(nil) + + // Add some test enums + p.enums = []EnumItem{ + { + Name: "StatusActive", + Value: "1", + Type: "pkg.Status", + Kind: constant.Int, + }, + { + Name: "StatusInactive", + Value: "0", + Type: "pkg.Status", + Kind: constant.Int, + }, + { + Name: "TypeA", + Value: "type_a", + Type: "pkg.Type", + Kind: constant.String, + }, + } + + result := p.Export() + t.AssertNE(result, "") + + // Parse the result to verify - use raw map to avoid gjson path issues with "." + var resultMap map[string][]interface{} + err := gjson.DecodeTo(result, &resultMap) + t.AssertNil(err) + + // Verify Status type has 2 values + statusValues := resultMap["pkg.Status"] + t.Assert(len(statusValues), 2) + + // Verify Type type has 1 value + typeValues := resultMap["pkg.Type"] + t.Assert(len(typeValues), 1) + t.Assert(typeValues[0], "type_a") + }) +} + +func Test_EnumsParser_Export_IntValues(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + p := NewEnumsParser(nil) + p.enums = []EnumItem{ + {Name: "One", Value: "1", Type: "pkg.Int", Kind: constant.Int}, + {Name: "Two", Value: "2", Type: "pkg.Int", Kind: constant.Int}, + {Name: "Negative", Value: "-5", Type: "pkg.Int", Kind: constant.Int}, + } + + result := p.Export() + var resultMap map[string][]interface{} + err := gjson.DecodeTo(result, &resultMap) + t.AssertNil(err) + + values := resultMap["pkg.Int"] + t.Assert(len(values), 3) + // Int values should be exported as integers (stored as float64 in JSON) + t.Assert(values[0], float64(1)) + t.Assert(values[1], float64(2)) + t.Assert(values[2], float64(-5)) + }) +} + +func Test_EnumsParser_Export_FloatValues(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + p := NewEnumsParser(nil) + p.enums = []EnumItem{ + {Name: "Pi", Value: "3.14159", Type: "pkg.Float", Kind: constant.Float}, + {Name: "E", Value: "2.71828", Type: "pkg.Float", Kind: constant.Float}, + } + + result := p.Export() + var resultMap map[string][]interface{} + err := gjson.DecodeTo(result, &resultMap) + t.AssertNil(err) + + values := resultMap["pkg.Float"] + t.Assert(len(values), 2) + }) +} + +func Test_EnumsParser_Export_BoolValues(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + p := NewEnumsParser(nil) + p.enums = []EnumItem{ + {Name: "True", Value: "true", Type: "pkg.Bool", Kind: constant.Bool}, + {Name: "False", Value: "false", Type: "pkg.Bool", Kind: constant.Bool}, + } + + result := p.Export() + var resultMap map[string][]interface{} + err := gjson.DecodeTo(result, &resultMap) + t.AssertNil(err) + + values := resultMap["pkg.Bool"] + t.Assert(len(values), 2) + t.Assert(values[0], true) + t.Assert(values[1], false) + }) +} + +func Test_EnumsParser_Export_StringValues(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + p := NewEnumsParser(nil) + p.enums = []EnumItem{ + {Name: "Hello", Value: "hello", Type: "pkg.Str", Kind: constant.String}, + {Name: "World", Value: "world", Type: "pkg.Str", Kind: constant.String}, + } + + result := p.Export() + var resultMap map[string][]interface{} + err := gjson.DecodeTo(result, &resultMap) + t.AssertNil(err) + + values := resultMap["pkg.Str"] + t.Assert(len(values), 2) + t.Assert(values[0], "hello") + t.Assert(values[1], "world") + }) +} + +func Test_EnumsParser_Export_MixedTypes(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + p := NewEnumsParser(nil) + p.enums = []EnumItem{ + {Name: "IntVal", Value: "42", Type: "pkg.IntType", Kind: constant.Int}, + {Name: "StrVal", Value: "test", Type: "pkg.StrType", Kind: constant.String}, + {Name: "BoolVal", Value: "true", Type: "pkg.BoolType", Kind: constant.Bool}, + } + + result := p.Export() + var resultMap map[string][]interface{} + err := gjson.DecodeTo(result, &resultMap) + t.AssertNil(err) + + // Each type should have its own array + t.Assert(len(resultMap["pkg.IntType"]), 1) + t.Assert(len(resultMap["pkg.StrType"]), 1) + t.Assert(len(resultMap["pkg.BoolType"]), 1) + }) +} + +func Test_EnumItem_Structure(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + // Test EnumItem structure + item := EnumItem{ + Name: "TestEnum", + Value: "test_value", + Type: "github.com/test/pkg.EnumType", + Kind: constant.String, + } + + t.Assert(item.Name, "TestEnum") + t.Assert(item.Value, "test_value") + t.Assert(item.Type, "github.com/test/pkg.EnumType") + t.Assert(item.Kind, constant.String) + }) +} + +func Test_EnumsParser_ParsePackages_Integration(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + // Create a temporary directory with a Go package containing enums + // Note: The module path must contain "/" for enums to be parsed + // (the parser skips std types without "/" in the type name) + tempDir := gfile.Temp(guid.S()) + err := gfile.Mkdir(tempDir) + t.AssertNil(err) + defer gfile.Remove(tempDir) + + // Create go.mod with a path containing "/" + goModContent := `module github.com/test/enumtest + +go 1.21 +` + err = gfile.PutContents(filepath.Join(tempDir, "go.mod"), goModContent) + t.AssertNil(err) + + // Create a Go file with enum definitions + enumsContent := `package enumtest + +type Status int + +const ( + StatusActive Status = 1 + StatusInactive Status = 0 +) + +type Color string + +const ( + ColorRed Color = "red" + ColorGreen Color = "green" + ColorBlue Color = "blue" +) +` + err = gfile.PutContents(filepath.Join(tempDir, "enums.go"), enumsContent) + t.AssertNil(err) + + // Load the package + cfg := &packages.Config{ + Dir: tempDir, + Mode: pkgLoadMode, + Tests: false, + } + pkgs, err := packages.Load(cfg) + t.AssertNil(err) + t.Assert(len(pkgs) > 0, true) + + // Parse the packages + p := NewEnumsParser(nil) + p.ParsePackages(pkgs) + + // Export and verify - result should contain parsed enums + result := p.Export() + // Verify the export contains some data + t.Assert(len(result) > 2, true) // More than just "{}" + + // Parse result as raw map to handle keys with "/" + var resultMap map[string][]interface{} + err = gjson.DecodeTo(result, &resultMap) + t.AssertNil(err) + + // Verify Status enum was parsed (type will be "github.com/test/enumtest.Status") + statusKey := "github.com/test/enumtest.Status" + statusValues, hasStatus := resultMap[statusKey] + t.Assert(hasStatus, true) + t.Assert(len(statusValues), 2) + + // Verify Color enum was parsed + colorKey := "github.com/test/enumtest.Color" + colorValues, hasColor := resultMap[colorKey] + t.Assert(hasColor, true) + t.Assert(len(colorValues), 3) + }) +} + +func Test_EnumsParser_ParsePackages_WithPrefixes(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + // Create a temporary directory with a Go package + tempDir := gfile.Temp(guid.S()) + err := gfile.Mkdir(tempDir) + t.AssertNil(err) + defer gfile.Remove(tempDir) + + // Create go.mod with a specific module name + goModContent := `module github.com/allowed/pkg + +go 1.21 +` + err = gfile.PutContents(filepath.Join(tempDir, "go.mod"), goModContent) + t.AssertNil(err) + + // Create a Go file with enum definitions + enumsContent := `package pkg + +type Status int + +const ( + StatusOK Status = 1 +) +` + err = gfile.PutContents(filepath.Join(tempDir, "enums.go"), enumsContent) + t.AssertNil(err) + + // Load the package + cfg := &packages.Config{ + Dir: tempDir, + Mode: pkgLoadMode, + Tests: false, + } + pkgs, err := packages.Load(cfg) + t.AssertNil(err) + + // Parse with prefix filter that matches + p := NewEnumsParser([]string{"github.com/allowed"}) + p.ParsePackages(pkgs) + + result := p.Export() + // Should have enums because prefix matches + t.AssertNE(result, "{}") + + // Parse with prefix filter that doesn't match + p2 := NewEnumsParser([]string{"github.com/other"}) + p2.ParsePackages(pkgs) + + result2 := p2.Export() + // Should be empty because prefix doesn't match + t.Assert(result2, "{}") + }) +} + +func Test_getStandardPackages(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + stdPkgs := getStandardPackages() + t.AssertNE(stdPkgs, nil) + t.Assert(len(stdPkgs) > 0, true) + + // Verify some common standard packages are included + _, hasFmt := stdPkgs["fmt"] + t.Assert(hasFmt, true) + + _, hasOs := stdPkgs["os"] + t.Assert(hasOs, true) + + _, hasContext := stdPkgs["context"] + t.Assert(hasContext, true) + }) +} diff --git a/cmd/gf/internal/cmd/geninit/geninit_ast.go b/cmd/gf/internal/cmd/geninit/geninit_ast.go index 1775a3a49..fde378291 100644 --- a/cmd/gf/internal/cmd/geninit/geninit_ast.go +++ b/cmd/gf/internal/cmd/geninit/geninit_ast.go @@ -79,7 +79,7 @@ func (r *ASTReplacer) ReplaceInFile(ctx context.Context, filePath string) error } // Write back to file without formatting. - // Formatting will be handled by utils.GoFmt after all replacements are done. + // Formatting will be handled by formatGoFiles after all replacements are done. var buf bytes.Buffer if err := printer.Fprint(&buf, r.fset, file); err != nil { return err diff --git a/cmd/gf/internal/cmd/geninit/geninit_generator.go b/cmd/gf/internal/cmd/geninit/geninit_generator.go index 7076cde9a..62fd395db 100644 --- a/cmd/gf/internal/cmd/geninit/geninit_generator.go +++ b/cmd/gf/internal/cmd/geninit/geninit_generator.go @@ -9,13 +9,13 @@ package geninit import ( "context" "fmt" + "go/format" "path/filepath" "github.com/gogf/gf/v2/os/gfile" "github.com/gogf/gf/v2/text/gstr" "github.com/gogf/gf/cmd/gf/v2/internal/utility/mlog" - "github.com/gogf/gf/cmd/gf/v2/internal/utility/utils" ) // generateProject copies the template to the destination and performs cleanup @@ -82,8 +82,10 @@ func generateProject(ctx context.Context, srcPath, name, oldModule, newModule st } } - // 6. Format the generated Go files - utils.GoFmt(dstPath) + // 6. Format the generated Go files using go/format (not imports.Process) + // Note: We use formatGoFiles instead of utils.GoFmt because imports.Process + // may incorrectly "fix" local import paths by replacing them with cached module paths. + formatGoFiles(dstPath) mlog.Print("Project generated successfully!") return nil @@ -112,3 +114,33 @@ func upgradeDependencies(ctx context.Context, projectDir string) error { mlog.Print("Dependencies upgraded successfully!") return nil } + +// formatGoFiles formats all Go files in the directory using go/format. +// Unlike imports.Process, this only formats code without modifying imports, +// which prevents incorrect "fixing" of local import paths. +func formatGoFiles(dir string) { + files, err := findGoFiles(dir) + if err != nil { + mlog.Printf("Failed to find Go files for formatting: %v", err) + return + } + + for _, file := range files { + content := gfile.GetContents(file) + if content == "" { + continue + } + + formatted, err := format.Source([]byte(content)) + if err != nil { + mlog.Debugf("Failed to format %s: %v", file, err) + continue + } + + if string(formatted) != content { + if err := gfile.PutContents(file, string(formatted)); err != nil { + mlog.Debugf("Failed to write formatted file %s: %v", file, err) + } + } + } +} diff --git a/cmd/gf/internal/cmd/geninit/geninit_z_unit_test.go b/cmd/gf/internal/cmd/geninit/geninit_z_unit_test.go new file mode 100644 index 000000000..6f05eddf0 --- /dev/null +++ b/cmd/gf/internal/cmd/geninit/geninit_z_unit_test.go @@ -0,0 +1,359 @@ +// Copyright GoFrame gf Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package geninit + +import ( + "context" + "path/filepath" + "testing" + + "github.com/gogf/gf/v2/os/gfile" + "github.com/gogf/gf/v2/test/gtest" + "github.com/gogf/gf/v2/util/guid" +) + +func Test_ParseGitURL_Basic(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + // Test basic github URL + info, err := ParseGitURL("github.com/gogf/gf") + t.AssertNil(err) + t.Assert(info.Host, "github.com") + t.Assert(info.Owner, "gogf") + t.Assert(info.Repo, "gf") + t.Assert(info.SubPath, "") + t.Assert(info.Branch, "main") + t.Assert(info.CloneURL, "https://github.com/gogf/gf.git") + }) +} + +func Test_ParseGitURL_WithHTTPS(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + // Test URL with https prefix + info, err := ParseGitURL("https://github.com/gogf/gf") + t.AssertNil(err) + t.Assert(info.Host, "github.com") + t.Assert(info.Owner, "gogf") + t.Assert(info.Repo, "gf") + }) +} + +func Test_ParseGitURL_WithGitSuffix(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + // Test URL with .git suffix + info, err := ParseGitURL("github.com/gogf/gf.git") + t.AssertNil(err) + t.Assert(info.Host, "github.com") + t.Assert(info.Owner, "gogf") + t.Assert(info.Repo, "gf") + }) +} + +func Test_ParseGitURL_WithSubPath(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + // Test URL with subdirectory + info, err := ParseGitURL("github.com/gogf/examples/httpserver/jwt") + t.AssertNil(err) + t.Assert(info.Host, "github.com") + t.Assert(info.Owner, "gogf") + t.Assert(info.Repo, "examples") + t.Assert(info.SubPath, "httpserver/jwt") + t.Assert(info.CloneURL, "https://github.com/gogf/examples.git") + }) +} + +func Test_ParseGitURL_WithTreeBranch(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + // Test GitHub web URL with /tree/branch/ + info, err := ParseGitURL("github.com/gogf/examples/tree/develop/httpserver/jwt") + t.AssertNil(err) + t.Assert(info.Host, "github.com") + t.Assert(info.Owner, "gogf") + t.Assert(info.Repo, "examples") + t.Assert(info.Branch, "develop") + t.Assert(info.SubPath, "httpserver/jwt") + }) +} + +func Test_ParseGitURL_WithVersion(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + // Test URL with version suffix + info, err := ParseGitURL("github.com/gogf/gf/cmd/gf/v2@v2.9.7") + t.AssertNil(err) + t.Assert(info.Host, "github.com") + t.Assert(info.Owner, "gogf") + t.Assert(info.Repo, "gf") + t.Assert(info.SubPath, "cmd/gf/v2") + }) +} + +func Test_ParseGitURL_Invalid(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + // Test invalid URL (too short) + _, err := ParseGitURL("github.com/gogf") + t.AssertNE(err, nil) + }) +} + +func Test_IsSubdirRepo_NotSubdir(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + // Standard Go module paths should not be detected as subdirectory + t.Assert(IsSubdirRepo("github.com/gogf/gf"), false) + t.Assert(IsSubdirRepo("github.com/gogf/gf/v2"), false) + }) +} + +func Test_IsSubdirRepo_GoModuleWithCmd(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + // Go module paths with common patterns should not be detected as subdirectory + t.Assert(IsSubdirRepo("github.com/gogf/gf/cmd/gf/v2"), false) + t.Assert(IsSubdirRepo("github.com/gogf/gf/contrib/drivers/mysql/v2"), false) + }) +} + +func Test_IsSubdirRepo_ActualSubdir(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + // Actual subdirectories should be detected + t.Assert(IsSubdirRepo("github.com/gogf/examples/httpserver/jwt"), true) + t.Assert(IsSubdirRepo("github.com/gogf/examples/grpc/basic"), true) + }) +} + +func Test_GetModuleNameFromGoMod_Valid(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + // Create temp directory with go.mod + tempDir := gfile.Temp(guid.S()) + err := gfile.Mkdir(tempDir) + t.AssertNil(err) + defer gfile.Remove(tempDir) + + // Write go.mod file + goModContent := `module github.com/test/myproject + +go 1.21 + +require ( + github.com/gogf/gf/v2 v2.9.0 +) +` + err = gfile.PutContents(filepath.Join(tempDir, "go.mod"), goModContent) + t.AssertNil(err) + + // Test extraction + moduleName := GetModuleNameFromGoMod(tempDir) + t.Assert(moduleName, "github.com/test/myproject") + }) +} + +func Test_GetModuleNameFromGoMod_NoFile(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + // Create temp directory without go.mod + tempDir := gfile.Temp(guid.S()) + err := gfile.Mkdir(tempDir) + t.AssertNil(err) + defer gfile.Remove(tempDir) + + // Test extraction - should return empty + moduleName := GetModuleNameFromGoMod(tempDir) + t.Assert(moduleName, "") + }) +} + +func Test_GetModuleNameFromGoMod_SimpleModule(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + // Create temp directory with simple go.mod + tempDir := gfile.Temp(guid.S()) + err := gfile.Mkdir(tempDir) + t.AssertNil(err) + defer gfile.Remove(tempDir) + + // Write simple go.mod file + goModContent := `module main + +go 1.21 +` + err = gfile.PutContents(filepath.Join(tempDir, "go.mod"), goModContent) + t.AssertNil(err) + + // Test extraction + moduleName := GetModuleNameFromGoMod(tempDir) + t.Assert(moduleName, "main") + }) +} + +func Test_ASTReplacer_ReplaceInFile(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + // Create temp directory + tempDir := gfile.Temp(guid.S()) + err := gfile.Mkdir(tempDir) + t.AssertNil(err) + defer gfile.Remove(tempDir) + + // Create a Go file with imports + goFileContent := `package main + +import ( + "fmt" + + "github.com/old/module/internal/service" + "github.com/old/module/pkg/utils" + "github.com/other/package" +) + +func main() { + fmt.Println("Hello") +} +` + goFilePath := filepath.Join(tempDir, "main.go") + err = gfile.PutContents(goFilePath, goFileContent) + t.AssertNil(err) + + // Replace imports + replacer := NewASTReplacer("github.com/old/module", "github.com/new/project") + err = replacer.ReplaceInFile(context.Background(), goFilePath) + t.AssertNil(err) + + // Verify replacement + content := gfile.GetContents(goFilePath) + t.Assert(gfile.Exists(goFilePath), true) + + // Check that old imports are replaced + t.AssertNE(content, "") + t.Assert(contains(content, `"github.com/new/project/internal/service"`), true) + t.Assert(contains(content, `"github.com/new/project/pkg/utils"`), true) + + // Check that other imports are not affected + t.Assert(contains(content, `"github.com/other/package"`), true) + t.Assert(contains(content, `"fmt"`), true) + }) +} + +func Test_ASTReplacer_ReplaceInDir(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + // Create temp directory structure + tempDir := gfile.Temp(guid.S()) + err := gfile.Mkdir(tempDir) + t.AssertNil(err) + defer gfile.Remove(tempDir) + + // Create subdirectory + subDir := filepath.Join(tempDir, "sub") + err = gfile.Mkdir(subDir) + t.AssertNil(err) + + // Create main.go + mainContent := `package main + +import "github.com/old/module/sub" + +func main() { + sub.Hello() +} +` + err = gfile.PutContents(filepath.Join(tempDir, "main.go"), mainContent) + t.AssertNil(err) + + // Create sub/sub.go + subContent := `package sub + +import "github.com/old/module/pkg" + +func Hello() { + pkg.Do() +} +` + err = gfile.PutContents(filepath.Join(subDir, "sub.go"), subContent) + t.AssertNil(err) + + // Replace imports in directory + replacer := NewASTReplacer("github.com/old/module", "github.com/new/project") + err = replacer.ReplaceInDir(context.Background(), tempDir) + t.AssertNil(err) + + // Verify main.go replacement + mainResult := gfile.GetContents(filepath.Join(tempDir, "main.go")) + t.Assert(contains(mainResult, `"github.com/new/project/sub"`), true) + + // Verify sub/sub.go replacement + subResult := gfile.GetContents(filepath.Join(subDir, "sub.go")) + t.Assert(contains(subResult, `"github.com/new/project/pkg"`), true) + }) +} + +func Test_findGoFiles(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + // Create temp directory structure + tempDir := gfile.Temp(guid.S()) + err := gfile.Mkdir(tempDir) + t.AssertNil(err) + defer gfile.Remove(tempDir) + + // Create subdirectories + subDir := filepath.Join(tempDir, "sub") + err = gfile.Mkdir(subDir) + t.AssertNil(err) + + // Create various files + err = gfile.PutContents(filepath.Join(tempDir, "main.go"), "package main") + t.AssertNil(err) + err = gfile.PutContents(filepath.Join(tempDir, "readme.md"), "# README") + t.AssertNil(err) + err = gfile.PutContents(filepath.Join(subDir, "sub.go"), "package sub") + t.AssertNil(err) + err = gfile.PutContents(filepath.Join(subDir, "data.json"), "{}") + t.AssertNil(err) + + // Find Go files + files, err := findGoFiles(tempDir) + t.AssertNil(err) + + // Should find exactly 2 Go files + t.Assert(len(files), 2) + + // Verify file names + hasMain := false + hasSub := false + for _, f := range files { + if filepath.Base(f) == "main.go" { + hasMain = true + } + if filepath.Base(f) == "sub.go" { + hasSub = true + } + } + t.Assert(hasMain, true) + t.Assert(hasSub, true) + }) +} + +func Test_findGoFiles_EmptyDir(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + // Create empty temp directory + tempDir := gfile.Temp(guid.S()) + err := gfile.Mkdir(tempDir) + t.AssertNil(err) + defer gfile.Remove(tempDir) + + // Find Go files + files, err := findGoFiles(tempDir) + t.AssertNil(err) + t.Assert(len(files), 0) + }) +} + +// Helper function to check if string contains substring +func contains(s, substr string) bool { + return len(s) >= len(substr) && (s == substr || len(s) > 0 && containsAt(s, substr)) +} + +func containsAt(s, substr string) bool { + for i := 0; i <= len(s)-len(substr); i++ { + if s[i:i+len(substr)] == substr { + return true + } + } + return false +} diff --git a/cmd/gf/internal/cmd/testdata/build/varmap/go.mod b/cmd/gf/internal/cmd/testdata/build/varmap/go.mod index 037f3b36a..e73a529d0 100644 --- a/cmd/gf/internal/cmd/testdata/build/varmap/go.mod +++ b/cmd/gf/internal/cmd/testdata/build/varmap/go.mod @@ -4,7 +4,7 @@ go 1.23.0 toolchain go1.24.6 -require github.com/gogf/gf/v2 v2.9.7 +require github.com/gogf/gf/v2 v2.9.8 require ( go.opentelemetry.io/otel v1.38.0 // indirect diff --git a/cmd/gf/internal/cmd/testdata/gendao/sharding/sharding_overlapping.sql b/cmd/gf/internal/cmd/testdata/gendao/sharding/sharding_overlapping.sql new file mode 100644 index 000000000..6e6a8e928 --- /dev/null +++ b/cmd/gf/internal/cmd/testdata/gendao/sharding/sharding_overlapping.sql @@ -0,0 +1,47 @@ +-- Test case for issue #4603: overlapping sharding patterns +-- https://github.com/gogf/gf/issues/4603 +-- +-- Patterns: "a_?", "a_b_?", "a_c_?" +-- Expected: a_1/a_2 -> "a", a_b_1/a_b_2 -> "a_b", a_c_1/a_c_2 -> "a_c" + +CREATE TABLE `a_1` +( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `name` varchar(45) NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +CREATE TABLE `a_2` +( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `name` varchar(45) NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +CREATE TABLE `a_b_1` +( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `name` varchar(45) NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +CREATE TABLE `a_b_2` +( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `name` varchar(45) NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +CREATE TABLE `a_c_1` +( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `name` varchar(45) NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +CREATE TABLE `a_c_2` +( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `name` varchar(45) NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; diff --git a/cmd/gf/internal/cmd/testdata/gendao/tables_pattern.sql b/cmd/gf/internal/cmd/testdata/gendao/tables_pattern.sql new file mode 100644 index 000000000..0d122de3a --- /dev/null +++ b/cmd/gf/internal/cmd/testdata/gendao/tables_pattern.sql @@ -0,0 +1,30 @@ +-- Test case for issue #4629: tables pattern matching +-- https://github.com/gogf/gf/issues/4629 +-- Standard SQL syntax compatible with MySQL and PostgreSQL +-- +-- Tables: trade_order, trade_item, user_info, user_log, config + +CREATE TABLE trade_order ( + id INTEGER PRIMARY KEY, + name VARCHAR(45) NOT NULL +); + +CREATE TABLE trade_item ( + id INTEGER PRIMARY KEY, + name VARCHAR(45) NOT NULL +); + +CREATE TABLE user_info ( + id INTEGER PRIMARY KEY, + name VARCHAR(45) NOT NULL +); + +CREATE TABLE user_log ( + id INTEGER PRIMARY KEY, + name VARCHAR(45) NOT NULL +); + +CREATE TABLE config ( + id INTEGER PRIMARY KEY, + name VARCHAR(45) NOT NULL +); diff --git a/cmd/gf/internal/cmd/testdata/genpb/multiple_tags.proto b/cmd/gf/internal/cmd/testdata/genpb/multiple_tags.proto new file mode 100644 index 000000000..0f15b4217 --- /dev/null +++ b/cmd/gf/internal/cmd/testdata/genpb/multiple_tags.proto @@ -0,0 +1,22 @@ +syntax = "proto3"; + +package genpb; + +option go_package = "genpb/v1"; + +message UserReq { + // v:required + // v:#Id > 0 + int64 Id = 1; + // User name for login + string Name = 2; + // v:required + // v:email + string Email = 3; // User email address +} + +message UserResp { + int64 Id = 1; + string Name = 2; + string Email = 3; +} diff --git a/cmd/gf/internal/cmd/testdata/genpb/nested_message.proto b/cmd/gf/internal/cmd/testdata/genpb/nested_message.proto new file mode 100644 index 000000000..c7a832f5d --- /dev/null +++ b/cmd/gf/internal/cmd/testdata/genpb/nested_message.proto @@ -0,0 +1,21 @@ +syntax = "proto3"; + +package genpb; + +option go_package = "genpb/v1"; + +message Order { + // v:required + int64 OrderId = 1; + // Order details + OrderDetail Detail = 2; +} + +message OrderDetail { + // v:required + string ProductName = 1; + // v:min:1 + int32 Quantity = 2; + // v:min:0.01 + double Price = 3; +} diff --git a/cmd/gf/internal/cmd/testdata/issue/4387/api/types.go b/cmd/gf/internal/cmd/testdata/issue/4387/api/types.go new file mode 100644 index 000000000..9af24e033 --- /dev/null +++ b/cmd/gf/internal/cmd/testdata/issue/4387/api/types.go @@ -0,0 +1,16 @@ +// Copyright GoFrame gf Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package api + +// Status is a sample enum type for testing. +type Status int + +const ( + StatusPending Status = iota + StatusActive + StatusDone +) diff --git a/cmd/gf/internal/cmd/testdata/issue/4387/go.mod b/cmd/gf/internal/cmd/testdata/issue/4387/go.mod new file mode 100644 index 000000000..f2ecd4c8d --- /dev/null +++ b/cmd/gf/internal/cmd/testdata/issue/4387/go.mod @@ -0,0 +1,3 @@ +module github.com/gogf/gf/cmd/gf/v2/internal/cmd/testdata/issue/4387 + +go 1.20 diff --git a/container/gmap/gmap_hash_k_v_map.go b/container/gmap/gmap_hash_k_v_map.go index 0b9f9c8ea..6d65d59e9 100644 --- a/container/gmap/gmap_hash_k_v_map.go +++ b/container/gmap/gmap_hash_k_v_map.go @@ -17,19 +17,29 @@ import ( "github.com/gogf/gf/v2/util/gconv" ) +// NilChecker is a function that checks whether the given value is nil. +type NilChecker[V any] func(V) bool + // KVMap wraps map type `map[K]V` and provides more map features. type KVMap[K comparable, V any] struct { - mu rwmutex.RWMutex - data map[K]V + mu rwmutex.RWMutex + data map[K]V + nilChecker NilChecker[V] } // NewKVMap creates and returns an empty hash map. -// The parameter `safe` is used to specify whether to use the map in concurrent-safety mode, -// which is false by default. +// The parameter `safe` is used to specify whether to use the map in concurrent-safety mode, which is false by default. func NewKVMap[K comparable, V any](safe ...bool) *KVMap[K, V] { return NewKVMapFrom(make(map[K]V), safe...) } +// NewKVMapWithChecker creates and returns an empty hash map with a custom nil checker. +// The parameter `checker` is a function used to determine if a value is nil. +// The parameter `safe` is used to specify whether to use the map in concurrent-safety mode, which is false by default. +func NewKVMapWithChecker[K comparable, V any](checker NilChecker[V], safe ...bool) *KVMap[K, V] { + return NewKVMapWithCheckerFrom(make(map[K]V), checker, safe...) +} + // NewKVMapFrom creates and returns a hash map from given map `data`. // Note that, the param `data` map will be set as the underlying data map (no deep copy), // there might be some concurrent-safe issues when changing the map outside. @@ -41,6 +51,37 @@ func NewKVMapFrom[K comparable, V any](data map[K]V, safe ...bool) *KVMap[K, V] return m } +// NewKVMapWithCheckerFrom creates and returns a hash map from given map `data` with a custom nil checker. +// Note that, the param `data` map will be set as the underlying data map (no deep copy), +// and there might be some concurrent-safe issues when changing the map outside. +// The parameter `checker` is a function used to determine if a value is nil. +// The parameter `safe` is used to specify whether to use the map in concurrent-safety mode, which is false by default. +func NewKVMapWithCheckerFrom[K comparable, V any](data map[K]V, checker NilChecker[V], safe ...bool) *KVMap[K, V] { + m := NewKVMapFrom[K, V](data, safe...) + m.RegisterNilChecker(checker) + return m +} + +// RegisterNilChecker registers a custom nil checker function for the map values. +// This function is used to determine if a value should be considered as nil. +// The nil checker function takes a value of type V and returns a boolean indicating +// whether the value should be treated as nil. +func (m *KVMap[K, V]) RegisterNilChecker(nilChecker NilChecker[V]) { + m.mu.Lock() + defer m.mu.Unlock() + m.nilChecker = nilChecker +} + +// isNil checks whether the given value is nil. +// It first checks if a custom nil checker function is registered and uses it if available, +// otherwise it performs a standard nil check using any(v) == nil. +func (m *KVMap[K, V]) isNil(v V) bool { + if m.nilChecker != nil { + return m.nilChecker(v) + } + return any(v) == nil +} + // Iterator iterates the hash map readonly with custom callback function `f`. // If `f` returns true, then it continues iterating; or false to stop. func (m *KVMap[K, V]) Iterator(f func(k K, v V) bool) { @@ -217,8 +258,7 @@ func (m *KVMap[K, V]) doSetWithLockCheck(key K, value V) (val V, ok bool) { if v, ok := m.data[key]; ok { return v, true } - - if any(value) != nil { + if !m.isNil(value) { m.data[key] = value } return value, false @@ -255,7 +295,7 @@ func (m *KVMap[K, V]) GetOrSetFuncLock(key K, f func() V) V { return v } value := f() - if any(value) != nil { + if !m.isNil(value) { m.data[key] = value } return value diff --git a/container/gmap/gmap_list_k_v_map.go b/container/gmap/gmap_list_k_v_map.go index 6bfe2a7e9..6b37ed57c 100644 --- a/container/gmap/gmap_list_k_v_map.go +++ b/container/gmap/gmap_list_k_v_map.go @@ -27,9 +27,10 @@ import ( // // Reference: http://en.wikipedia.org/wiki/Associative_array type ListKVMap[K comparable, V any] struct { - mu rwmutex.RWMutex - data map[K]*glist.TElement[*gListKVMapNode[K, V]] - list *glist.TList[*gListKVMapNode[K, V]] + mu rwmutex.RWMutex + data map[K]*glist.TElement[*gListKVMapNode[K, V]] + list *glist.TList[*gListKVMapNode[K, V]] + nilChecker NilChecker[V] } type gListKVMapNode[K comparable, V any] struct { @@ -49,6 +50,16 @@ func NewListKVMap[K comparable, V any](safe ...bool) *ListKVMap[K, V] { } } +// NewListKVMapWithChecker creates and returns a new ListKVMap instance with a custom nil checker. +// The parameter `checker` is a function used to determine if a value is nil. +// The parameter `safe` is used to specify whether using map in concurrent-safety, +// which is false by default. +func NewListKVMapWithChecker[K comparable, V any](checker NilChecker[V], safe ...bool) *ListKVMap[K, V] { + m := NewListKVMap[K, V](safe...) + m.RegisterNilChecker(checker) + return m +} + // NewListKVMapFrom returns a link map from given map `data`. // Note that, the param `data` map will be copied to the underlying data structure, // so changes to the original map will not affect the link map. @@ -58,6 +69,38 @@ func NewListKVMapFrom[K comparable, V any](data map[K]V, safe ...bool) *ListKVMa return m } +// NewListKVMapWithCheckerFrom returns a link map from given map `data` with a custom nil checker. +// Note that, the param `data` map will be copied to the underlying data structure, +// so changes to the original map will not affect the link map. +// The parameter `checker` is a function used to determine if a value is nil. +// The parameter `safe` is used to specify whether using map in concurrent-safety, +// which is false by default. +func NewListKVMapWithCheckerFrom[K comparable, V any](data map[K]V, nilChecker NilChecker[V], safe ...bool) *ListKVMap[K, V] { + m := NewListKVMapWithChecker[K, V](nilChecker, safe...) + m.Sets(data) + return m +} + +// RegisterNilChecker registers a custom nil checker function for the map values. +// This function is used to determine if a value should be considered as nil. +// The nil checker function takes a value of type V and returns a boolean indicating +// whether the value should be treated as nil. +func (m *ListKVMap[K, V]) RegisterNilChecker(nilChecker NilChecker[V]) { + m.mu.Lock() + defer m.mu.Unlock() + m.nilChecker = nilChecker +} + +// isNil checks whether the given value is nil. +// It first checks if a custom nil checker function is registered and uses it if available, +// otherwise it performs a standard nil check using any(v) == nil. +func (m *ListKVMap[K, V]) isNil(v V) bool { + if m.nilChecker != nil { + return m.nilChecker(v) + } + return any(v) == nil +} + // Iterator is alias of IteratorAsc. func (m *ListKVMap[K, V]) Iterator(f func(key K, value V) bool) { m.IteratorAsc(f) @@ -282,7 +325,7 @@ func (m *ListKVMap[K, V]) doSetWithLockCheckWithoutLock(key K, value V) V { if e, ok := m.data[key]; ok { return e.Value.value } - if any(value) != nil { + if !m.isNil(value) { m.data[key] = m.list.PushBack(&gListKVMapNode[K, V]{key, value}) } return value @@ -327,7 +370,7 @@ func (m *ListKVMap[K, V]) GetOrSetFuncLock(key K, f func() V) V { return e.Value.value } value := f() - if any(value) != nil { + if !m.isNil(value) { m.data[key] = m.list.PushBack(&gListKVMapNode[K, V]{key, value}) } return value @@ -370,7 +413,7 @@ func (m *ListKVMap[K, V]) SetIfNotExist(key K, value V) bool { if _, ok := m.data[key]; ok { return false } - if any(value) != nil { + if !m.isNil(value) { m.data[key] = m.list.PushBack(&gListKVMapNode[K, V]{key, value}) } return true @@ -390,7 +433,7 @@ func (m *ListKVMap[K, V]) SetIfNotExistFunc(key K, f func() V) bool { return false } value := f() - if any(value) != nil { + if !m.isNil(value) { m.data[key] = m.list.PushBack(&gListKVMapNode[K, V]{key, value}) } return true @@ -413,7 +456,7 @@ func (m *ListKVMap[K, V]) SetIfNotExistFuncLock(key K, f func() V) bool { return false } value := f() - if any(value) != nil { + if !m.isNil(value) { m.data[key] = m.list.PushBack(&gListKVMapNode[K, V]{key, value}) } return true diff --git a/container/gmap/gmap_z_unit_k_v_map_test.go b/container/gmap/gmap_z_unit_k_v_map_test.go index faef2c316..4bd977959 100644 --- a/container/gmap/gmap_z_unit_k_v_map_test.go +++ b/container/gmap/gmap_z_unit_k_v_map_test.go @@ -1630,3 +1630,67 @@ func Test_KVMap_Flip_String(t *testing.T) { t.Assert(m.Get("val2"), "key2") }) } + +// Test TypedNil with custom nil checker for pointers +func Test_KVMap_TypedNil(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + type Student struct { + Name string + Age int + } + m1 := gmap.NewKVMap[int, *Student](true) + for i := 0; i < 10; i++ { + m1.GetOrSetFuncLock(i, func() *Student { + if i%2 == 0 { + return &Student{} + } + return nil + }) + } + t.Assert(m1.Size(), 10) + m2 := gmap.NewKVMap[int, *Student](true) + m2.RegisterNilChecker(func(student *Student) bool { + return student == nil + }) + for i := 0; i < 10; i++ { + m2.GetOrSetFuncLock(i, func() *Student { + if i%2 == 0 { + return &Student{} + } + return nil + }) + } + t.Assert(m2.Size(), 5) + }) +} + +func Test_NewKVMapWithChecker_TypedNil(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + type Student struct { + Name string + Age int + } + m1 := gmap.NewKVMap[int, *Student](true) + for i := 0; i < 10; i++ { + m1.GetOrSetFuncLock(i, func() *Student { + if i%2 == 0 { + return &Student{} + } + return nil + }) + } + t.Assert(m1.Size(), 10) + m2 := gmap.NewKVMapWithChecker[int, *Student](func(student *Student) bool { + return student == nil + }, true) + for i := 0; i < 10; i++ { + m2.GetOrSetFuncLock(i, func() *Student { + if i%2 == 0 { + return &Student{} + } + return nil + }) + } + t.Assert(m2.Size(), 5) + }) +} diff --git a/container/gmap/gmap_z_unit_list_k_v_map_test.go b/container/gmap/gmap_z_unit_list_k_v_map_test.go index 7714f532f..5b9db095f 100644 --- a/container/gmap/gmap_z_unit_list_k_v_map_test.go +++ b/container/gmap/gmap_z_unit_list_k_v_map_test.go @@ -1341,3 +1341,67 @@ func Test_ListKVMap_UnmarshalValue_NilData(t *testing.T) { t.Assert(m.Get("b"), "2") }) } + +// Test typed nil values +func Test_ListKVMap_TypedNil(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + type Student struct { + Name string + Age int + } + m1 := gmap.NewListKVMap[int, *Student](true) + for i := 0; i < 10; i++ { + m1.GetOrSetFuncLock(i, func() *Student { + if i%2 == 0 { + return &Student{} + } + return nil + }) + } + t.Assert(m1.Size(), 10) + m2 := gmap.NewListKVMap[int, *Student](true) + m2.RegisterNilChecker(func(student *Student) bool { + return student == nil + }) + for i := 0; i < 10; i++ { + m2.GetOrSetFuncLock(i, func() *Student { + if i%2 == 0 { + return &Student{} + } + return nil + }) + } + t.Assert(m2.Size(), 5) + }) +} + +func Test_NewListKVMapWithChecker_TypedNil(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + type Student struct { + Name string + Age int + } + m1 := gmap.NewListKVMap[int, *Student](true) + for i := 0; i < 10; i++ { + m1.GetOrSetFuncLock(i, func() *Student { + if i%2 == 0 { + return &Student{} + } + return nil + }) + } + t.Assert(m1.Size(), 10) + m2 := gmap.NewListKVMapWithChecker[int, *Student](func(student *Student) bool { + return student == nil + }, true) + for i := 0; i < 10; i++ { + m2.GetOrSetFuncLock(i, func() *Student { + if i%2 == 0 { + return &Student{} + } + return nil + }) + } + t.Assert(m2.Size(), 5) + }) +} diff --git a/container/gset/gset_t_set.go b/container/gset/gset_t_set.go index ae3507cec..02ffbf6cd 100644 --- a/container/gset/gset_t_set.go +++ b/container/gset/gset_t_set.go @@ -15,10 +15,14 @@ import ( "github.com/gogf/gf/v2/util/gconv" ) +// NilChecker is a function that checks whether the given value is nil. +type NilChecker[T any] func(T) bool + // TSet[T] is consisted of any items. type TSet[T comparable] struct { - mu rwmutex.RWMutex - data map[T]struct{} + mu rwmutex.RWMutex + data map[T]struct{} + nilChecker NilChecker[T] } // NewTSet creates and returns a new set, which contains un-repeated items. @@ -30,6 +34,15 @@ func NewTSet[T comparable](safe ...bool) *TSet[T] { } } +// NewTSetWithChecker creates and returns a new set with a custom nil checker. +// The parameter `nilChecker` is a function used to determine if a value is nil. +// The parameter `safe` is used to specify whether using set in concurrent-safety mode. +func NewTSetWithChecker[T comparable](checker NilChecker[T], safe ...bool) *TSet[T] { + s := NewTSet[T](safe...) + s.RegisterNilChecker(checker) + return s +} + // NewTSetFrom returns a new set from `items`. // `items` - A slice of type T. func NewTSetFrom[T comparable](items []T, safe ...bool) *TSet[T] { @@ -43,6 +56,36 @@ func NewTSetFrom[T comparable](items []T, safe ...bool) *TSet[T] { } } +// NewTSetWithCheckerFrom returns a new set from `items` with a custom nil checker. +// The parameter `items` is a slice of elements to be added to the set. +// The parameter `checker` is a function used to determine if a value is nil. +// The parameter `safe` is used to specify whether using set in concurrent-safety mode. +func NewTSetWithCheckerFrom[T comparable](items []T, checker NilChecker[T], safe ...bool) *TSet[T] { + set := NewTSetWithChecker[T](checker, safe...) + set.Add(items...) + return set +} + +// RegisterNilChecker registers a custom nil checker function for the set elements. +// This function is used to determine if an element should be considered as nil. +// The nil checker function takes an element of type T and returns a boolean indicating +// whether the element should be treated as nil. +func (set *TSet[T]) RegisterNilChecker(nilChecker NilChecker[T]) { + set.mu.Lock() + defer set.mu.Unlock() + set.nilChecker = nilChecker +} + +// isNil checks whether the given value is nil. +// It first checks if a custom nil checker function is registered and uses it if available, +// otherwise it performs a standard nil check using any(v) == nil. +func (set *TSet[T]) isNil(v T) bool { + if set.nilChecker != nil { + return set.nilChecker(v) + } + return any(v) == nil +} + // Iterator iterates the set readonly with given callback function `f`, // if `f` returns true then continue iterating; or false to stop. func (set *TSet[T]) Iterator(f func(v T) bool) { @@ -56,13 +99,13 @@ func (set *TSet[T]) Iterator(f func(v T) bool) { // Add adds one or multiple items to the set. func (set *TSet[T]) Add(items ...T) { set.mu.Lock() + defer set.mu.Unlock() if set.data == nil { set.data = make(map[T]struct{}) } for _, v := range items { set.data[v] = struct{}{} } - set.mu.Unlock() } // AddIfNotExist checks whether item exists in the set, @@ -71,7 +114,7 @@ func (set *TSet[T]) Add(items ...T) { // // Note that, if `item` is nil, it does nothing and returns false. func (set *TSet[T]) AddIfNotExist(item T) bool { - if any(item) == nil { + if set.isNil(item) { return false } if !set.Contains(item) { @@ -95,7 +138,7 @@ func (set *TSet[T]) AddIfNotExist(item T) bool { // Note that, if `item` is nil, it does nothing and returns false. The function `f` // is executed without writing lock. func (set *TSet[T]) AddIfNotExistFunc(item T, f func() bool) bool { - if any(item) == nil { + if set.isNil(item) { return false } if !set.Contains(item) { @@ -121,7 +164,7 @@ func (set *TSet[T]) AddIfNotExistFunc(item T, f func() bool) bool { // Note that, if `item` is nil, it does nothing and returns false. The function `f` // is executed within writing lock. func (set *TSet[T]) AddIfNotExistFuncLock(item T, f func() bool) bool { - if any(item) == nil { + if set.isNil(item) { return false } if !set.Contains(item) { diff --git a/container/gset/gset_z_unit_t_set_test.go b/container/gset/gset_z_unit_t_set_test.go index 4db85fb13..10558a707 100644 --- a/container/gset/gset_z_unit_t_set_test.go +++ b/container/gset/gset_z_unit_t_set_test.go @@ -591,3 +591,42 @@ func TestTSet_RLockFunc(t *testing.T) { t.Assert(sum, 6) }) } + +func Test_TSet_TypedNil(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + type Student struct { + Name string + Age int + } + set := gset.NewTSet[*Student](true) + var s *Student = nil + exist := set.AddIfNotExist(s) + t.Assert(exist, true) + + set2 := gset.NewTSet[*Student](true) + set2.RegisterNilChecker(func(student *Student) bool { + return student == nil + }) + exist2 := set2.AddIfNotExist(s) + t.Assert(exist2, false) + }) +} + +func Test_NewTSetWithChecker_TypedNil(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + type Student struct { + Name string + Age int + } + set := gset.NewTSet[*Student](true) + var s *Student = nil + exist := set.AddIfNotExist(s) + t.Assert(exist, true) + + set2 := gset.NewTSetWithChecker[*Student](func(student *Student) bool { + return student == nil + }, true) + exist2 := set2.AddIfNotExist(s) + t.Assert(exist2, false) + }) +} diff --git a/container/gtree/gtree_k_v_avltree.go b/container/gtree/gtree_k_v_avltree.go index 323097039..661364751 100644 --- a/container/gtree/gtree_k_v_avltree.go +++ b/container/gtree/gtree_k_v_avltree.go @@ -18,11 +18,15 @@ import ( "github.com/gogf/gf/v2/util/gconv" ) +// NilChecker is a function that checks whether the given value is nil. +type NilChecker[V any] func(V) bool + // AVLKVTree holds elements of the AVL tree. type AVLKVTree[K comparable, V any] struct { mu rwmutex.RWMutex comparator func(v1, v2 K) int tree *avltree.Tree[K, V] + nilChecker NilChecker[V] } // AVLKVTreeNode is a single element within the tree. @@ -43,6 +47,15 @@ func NewAVLKVTree[K comparable, V any](comparator func(v1, v2 K) int, safe ...bo } } +// NewAVLKVTreeWithChecker instantiates an AVL tree with the custom key comparator and nil checker. +// The parameter `safe` is used to specify whether using tree in concurrent-safety, which is false in default. +// The parameter `checker` is used to specify whether the given value is nil. +func NewAVLKVTreeWithChecker[K comparable, V any](comparator func(v1, v2 K) int, checker NilChecker[V], safe ...bool) *AVLKVTree[K, V] { + t := NewAVLKVTree[K, V](comparator, safe...) + t.RegisterNilChecker(checker) + return t +} + // NewAVLKVTreeFrom instantiates an AVL tree with the custom key comparator and data map. // // The parameter `safe` is used to specify whether using tree in concurrent-safety, which is false in default. @@ -54,6 +67,37 @@ func NewAVLKVTreeFrom[K comparable, V any](comparator func(v1, v2 K) int, data m return tree } +// NewAVLKVTreeWithCheckerFrom instantiates an AVL tree with the custom key comparator, nil checker and data map. +// The parameter `safe` is used to specify whether using tree in concurrent-safety, which is false in default. +// The parameter `checker` is used to specify whether the given value is nil. +func NewAVLKVTreeWithCheckerFrom[K comparable, V any](comparator func(v1, v2 K) int, data map[K]V, checker NilChecker[V], safe ...bool) *AVLKVTree[K, V] { + tree := NewAVLKVTreeWithChecker[K, V](comparator, checker, safe...) + for k, v := range data { + tree.doSet(k, v) + } + return tree +} + +// RegisterNilChecker registers a custom nil checker function for the map values. +// This function is used to determine if a value should be considered as nil. +// The nil checker function takes a value of type V and returns a boolean indicating +// whether the value should be treated as nil. +func (tree *AVLKVTree[K, V]) RegisterNilChecker(nilChecker NilChecker[V]) { + tree.mu.Lock() + defer tree.mu.Unlock() + tree.nilChecker = nilChecker +} + +// isNil checks whether the given value is nil. +// It first checks if a custom nil checker function is registered and uses it if available, +// otherwise it performs a standard nil check using any(v) == nil. +func (tree *AVLKVTree[K, V]) isNil(value V) bool { + if tree.nilChecker != nil { + return tree.nilChecker(value) + } + return any(value) == nil +} + // Clone clones and returns a new tree from current tree. func (tree *AVLKVTree[K, V]) Clone() *AVLKVTree[K, V] { if tree == nil { @@ -518,7 +562,7 @@ func (tree *AVLKVTree[K, V]) Flip(comparator ...func(v1, v2 K) int) { // // It returns value with given `key`. func (tree *AVLKVTree[K, V]) doSet(key K, value V) V { - if any(value) == nil { + if tree.isNil(value) { return value } tree.tree.Put(key, value) diff --git a/container/gtree/gtree_k_v_btree.go b/container/gtree/gtree_k_v_btree.go index e1a4399ec..2adcede1b 100644 --- a/container/gtree/gtree_k_v_btree.go +++ b/container/gtree/gtree_k_v_btree.go @@ -24,6 +24,7 @@ type BKVTree[K comparable, V any] struct { comparator func(v1, v2 K) int m int // order (maximum number of children) tree *btree.Tree[K, V] + nilChecker NilChecker[V] } // BKVTreeEntry represents the key-value pair contained within nodes. @@ -45,6 +46,15 @@ func NewBKVTree[K comparable, V any](m int, comparator func(v1, v2 K) int, safe } } +// NewBKVTreeWithChecker instantiates a B-tree with `m` (maximum number of children), a custom key comparator and nil checker. +// The parameter `safe` is used to specify whether using tree in concurrent-safety, which is false in default. +// The parameter `checker` is used to specify whether the given value is nil. +func NewBKVTreeWithChecker[K comparable, V any](m int, comparator func(v1, v2 K) int, checker NilChecker[V], safe ...bool) *BKVTree[K, V] { + t := NewBKVTree[K, V](m, comparator, safe...) + t.RegisterNilChecker(checker) + return t +} + // NewBKVTreeFrom instantiates a B-tree with `m` (maximum number of children), a custom key comparator and data map. // The parameter `safe` is used to specify whether using tree in concurrent-safety, // which is false in default. @@ -56,6 +66,37 @@ func NewBKVTreeFrom[K comparable, V any](m int, comparator func(v1, v2 K) int, d return tree } +// NewBKVTreeWithCheckerFrom instantiates a B-tree with `m` (maximum number of children), a custom key comparator, nil checker and data map. +// The parameter `safe` is used to specify whether using tree in concurrent-safety, which is false in default. +// The parameter `checker` is used to specify whether the given value is nil. +func NewBKVTreeWithCheckerFrom[K comparable, V any](m int, comparator func(v1, v2 K) int, data map[K]V, checker NilChecker[V], safe ...bool) *BKVTree[K, V] { + tree := NewBKVTreeWithChecker[K, V](m, comparator, checker, safe...) + for k, v := range data { + tree.doSet(k, v) + } + return tree +} + +// RegisterNilChecker registers a custom nil checker function for the map values. +// This function is used to determine if a value should be considered as nil. +// The nil checker function takes a value of type V and returns a boolean indicating +// whether the value should be treated as nil. +func (tree *BKVTree[K, V]) RegisterNilChecker(nilChecker NilChecker[V]) { + tree.mu.Lock() + defer tree.mu.Unlock() + tree.nilChecker = nilChecker +} + +// isNil checks whether the given value is nil. +// It first checks if a custom nil checker function is registered and uses it if available, +// otherwise it performs a standard nil check using any(v) == nil. +func (tree *BKVTree[K, V]) isNil(value V) bool { + if tree.nilChecker != nil { + return tree.nilChecker(value) + } + return any(value) == nil +} + // Clone clones and returns a new tree from current tree. func (tree *BKVTree[K, V]) Clone() *BKVTree[K, V] { if tree == nil { @@ -453,7 +494,7 @@ func (tree *BKVTree[K, V]) Right() *BKVTreeEntry[K, V] { // // It returns value with given `key`. func (tree *BKVTree[K, V]) doSet(key K, value V) V { - if any(value) == nil { + if tree.isNil(value) { return value } tree.tree.Put(key, value) diff --git a/container/gtree/gtree_k_v_redblacktree.go b/container/gtree/gtree_k_v_redblacktree.go index 5b0be020b..47585716a 100644 --- a/container/gtree/gtree_k_v_redblacktree.go +++ b/container/gtree/gtree_k_v_redblacktree.go @@ -24,6 +24,7 @@ type RedBlackKVTree[K comparable, V any] struct { mu rwmutex.RWMutex comparator func(v1, v2 K) int tree *redblacktree.Tree[K, V] + nilChecker NilChecker[V] } // RedBlackKVTreeNode is a single element within the tree. @@ -41,6 +42,15 @@ func NewRedBlackKVTree[K comparable, V any](comparator func(v1, v2 K) int, safe return &tree } +// NewRedBlackKVTreeWithChecker instantiates a red-black tree with the custom key comparator and `nilChecker`. +// The parameter `safe` is used to specify whether using tree in concurrent-safety, which is false in default. +// The parameter `checker` is used to specify whether the given value is nil. +func NewRedBlackKVTreeWithChecker[K comparable, V any](comparator func(v1, v2 K) int, checker NilChecker[V], safe ...bool) *RedBlackKVTree[K, V] { + t := NewRedBlackKVTree[K, V](comparator, safe...) + t.RegisterNilChecker(checker) + return t +} + // NewRedBlackKVTreeFrom instantiates a red-black tree with the custom key comparator and `data` map. // The parameter `safe` is used to specify whether using tree in concurrent-safety, // which is false in default. @@ -50,6 +60,17 @@ func NewRedBlackKVTreeFrom[K comparable, V any](comparator func(v1, v2 K) int, d return &tree } +// NewRedBlackKVTreeWithCheckerFrom instantiates a red-black tree with the custom key comparator, `data` map and `nilChecker`. +// The parameter `safe` is used to specify whether using tree in concurrent-safety, which is false in default. +// The parameter `checker` is used to specify whether the given value is nil. +func NewRedBlackKVTreeWithCheckerFrom[K comparable, V any](comparator func(v1, v2 K) int, data map[K]V, checker NilChecker[V], safe ...bool) *RedBlackKVTree[K, V] { + t := NewRedBlackKVTreeWithChecker[K, V](comparator, checker, safe...) + for k, v := range data { + t.doSet(k, v) + } + return t +} + // RedBlackKVTreeInit instantiates a red-black tree with the custom key comparator. // The parameter `safe` is used to specify whether using tree in concurrent-safety, // which is false in default. @@ -75,6 +96,26 @@ func RedBlackKVTreeInitFrom[K comparable, V any](tree *RedBlackKVTree[K, V], com } } +// RegisterNilChecker registers a custom nil checker function for the map values. +// This function is used to determine if a value should be considered as nil. +// The nil checker function takes a value of type V and returns a boolean indicating +// whether the value should be treated as nil. +func (tree *RedBlackKVTree[K, V]) RegisterNilChecker(nilChecker NilChecker[V]) { + tree.mu.Lock() + defer tree.mu.Unlock() + tree.nilChecker = nilChecker +} + +// isNil checks whether the given value is nil. +// It first checks if a custom nil checker function is registered and uses it if available, +// otherwise it performs a standard nil check using any(v) == nil. +func (tree *RedBlackKVTree[K, V]) isNil(value V) bool { + if tree.nilChecker != nil { + return tree.nilChecker(value) + } + return any(value) == nil +} + // SetComparator sets/changes the comparator for sorting. func (tree *RedBlackKVTree[K, V]) SetComparator(comparator func(a, b K) int) { tree.comparator = comparator @@ -189,7 +230,7 @@ func (tree *RedBlackKVTree[K, V]) GetOrSetFunc(key K, f func() V) V { // GetOrSetFuncLock returns its `value` of `key`, or sets value with returned value of callback function `f` if it does // not exist and then returns this value. // -// GetOrSetFuncLock differs with GetOrSetFunc function is that it executes function `f`within mutex lock. +// GetOrSetFuncLock differs with GetOrSetFunc function is that it executes function `f` within mutex lock. func (tree *RedBlackKVTree[K, V]) GetOrSetFuncLock(key K, f func() V) V { tree.mu.Lock() defer tree.mu.Unlock() @@ -592,7 +633,7 @@ func (tree *RedBlackKVTree[K, V]) UnmarshalValue(value any) (err error) { // // It returns value with given `key`. func (tree *RedBlackKVTree[K, V]) doSet(key K, value V) (ret V) { - if any(value) == nil { + if tree.isNil(value) { return } tree.tree.Put(key, value) diff --git a/container/gtree/gtree_redblacktree.go b/container/gtree/gtree_redblacktree.go index 25138b243..9735075fb 100644 --- a/container/gtree/gtree_redblacktree.go +++ b/container/gtree/gtree_redblacktree.go @@ -46,7 +46,7 @@ func NewRedBlackTreeFrom(comparator func(v1, v2 any) int, data map[any]any, safe func (tree *RedBlackTree) lazyInit() { tree.once.Do(func() { if tree.RedBlackKVTree == nil { - tree.RedBlackKVTree = NewRedBlackKVTree[any, any](gutil.ComparatorTStr, false) + tree.RedBlackKVTree = NewRedBlackKVTree[any, any](gutil.ComparatorTStr[any], false) } }) } diff --git a/container/gtree/gtree_z_k_v_tree_test.go b/container/gtree/gtree_z_k_v_tree_test.go new file mode 100644 index 000000000..a769bf9b8 --- /dev/null +++ b/container/gtree/gtree_z_k_v_tree_test.go @@ -0,0 +1,210 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package gtree_test + +import ( + "testing" + + "github.com/gogf/gf/v2/container/gtree" + "github.com/gogf/gf/v2/test/gtest" + "github.com/gogf/gf/v2/util/gutil" +) + +func Test_KVAVLTree_TypedNil(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + type Student struct { + Name string + Age int + } + avlTree := gtree.NewAVLKVTree[int, *Student](gutil.ComparatorTStr[int], true) + for i := 0; i < 10; i++ { + if i%2 == 0 { + avlTree.Set(i, &Student{}) + } else { + var s *Student = nil + avlTree.Set(i, s) + } + } + t.Assert(avlTree.Size(), 10) + avlTree2 := gtree.NewAVLKVTree[int, *Student](gutil.ComparatorTStr[int], true) + avlTree2.RegisterNilChecker(func(student *Student) bool { + return student == nil + }) + for i := 0; i < 10; i++ { + if i%2 == 0 { + avlTree2.Set(i, &Student{}) + } else { + var s *Student = nil + avlTree2.Set(i, s) + } + } + t.Assert(avlTree2.Size(), 5) + + }) +} + +func Test_KVBTree_TypedNil(t *testing.T) { + type Student struct { + Name string + Age int + } + gtest.C(t, func(t *gtest.T) { + btree := gtree.NewBKVTree[int, *Student](100, gutil.ComparatorTStr[int], true) + for i := 0; i < 10; i++ { + if i%2 == 0 { + btree.Set(i, &Student{}) + } else { + var s *Student = nil + btree.Set(i, s) + } + } + t.Assert(btree.Size(), 10) + btree2 := gtree.NewBKVTree[int, *Student](100, gutil.ComparatorTStr[int], true) + btree2.RegisterNilChecker(func(student *Student) bool { + return student == nil + }) + for i := 0; i < 10; i++ { + if i%2 == 0 { + btree2.Set(i, &Student{}) + } else { + var s *Student = nil + btree2.Set(i, s) + } + } + t.Assert(btree2.Size(), 5) + }) + +} + +func Test_KVRedBlackTree_TypedNil(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + type Student struct { + Name string + Age int + } + redBlackTree := gtree.NewRedBlackKVTree[int, *Student](gutil.ComparatorTStr[int], true) + for i := 0; i < 10; i++ { + if i%2 == 0 { + redBlackTree.Set(i, &Student{}) + } else { + var s *Student = nil + redBlackTree.Set(i, s) + } + } + t.Assert(redBlackTree.Size(), 10) + redBlackTree2 := gtree.NewRedBlackKVTree[int, *Student](gutil.ComparatorTStr[int], true) + + redBlackTree2.RegisterNilChecker(func(student *Student) bool { + return student == nil + }) + for i := 0; i < 10; i++ { + if i%2 == 0 { + redBlackTree2.Set(i, &Student{}) + } else { + var s *Student = nil + redBlackTree2.Set(i, s) + } + } + t.Assert(redBlackTree2.Size(), 5) + }) +} + +func Test_NewKVAVLTreeWithChecker_TypedNil(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + type Student struct { + Name string + Age int + } + avlTree := gtree.NewAVLKVTree[int, *Student](gutil.ComparatorTStr[int], true) + for i := 0; i < 10; i++ { + if i%2 == 0 { + avlTree.Set(i, &Student{}) + } else { + var s *Student = nil + avlTree.Set(i, s) + } + } + t.Assert(avlTree.Size(), 10) + avlTree2 := gtree.NewAVLKVTreeWithChecker[int, *Student](gutil.ComparatorTStr[int], func(student *Student) bool { + return student == nil + }, true) + for i := 0; i < 10; i++ { + if i%2 == 0 { + avlTree2.Set(i, &Student{}) + } else { + var s *Student = nil + avlTree2.Set(i, s) + } + } + t.Assert(avlTree2.Size(), 5) + + }) +} + +func Test_NewKVBTreeWithChecker_TypedNil(t *testing.T) { + type Student struct { + Name string + Age int + } + gtest.C(t, func(t *gtest.T) { + btree := gtree.NewBKVTree[int, *Student](100, gutil.ComparatorTStr[int], true) + for i := 0; i < 10; i++ { + if i%2 == 0 { + btree.Set(i, &Student{}) + } else { + var s *Student = nil + btree.Set(i, s) + } + } + t.Assert(btree.Size(), 10) + btree2 := gtree.NewBKVTreeWithChecker[int, *Student](100, gutil.ComparatorTStr[int], func(student *Student) bool { + return student == nil + }, true) + for i := 0; i < 10; i++ { + if i%2 == 0 { + btree2.Set(i, &Student{}) + } else { + var s *Student = nil + btree2.Set(i, s) + } + } + t.Assert(btree2.Size(), 5) + }) + +} + +func Test_NewRedBlackKVTreeWithChecker_TypedNil(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + type Student struct { + Name string + Age int + } + redBlackTree := gtree.NewRedBlackKVTree[int, *Student](gutil.ComparatorTStr[int], true) + for i := 0; i < 10; i++ { + if i%2 == 0 { + redBlackTree.Set(i, &Student{}) + } else { + var s *Student = nil + redBlackTree.Set(i, s) + } + } + t.Assert(redBlackTree.Size(), 10) + redBlackTree2 := gtree.NewRedBlackKVTreeWithChecker[int, *Student](gutil.ComparatorTStr[int], func(student *Student) bool { + return student == nil + }, true) + + for i := 0; i < 10; i++ { + if i%2 == 0 { + redBlackTree2.Set(i, &Student{}) + } else { + var s *Student = nil + redBlackTree2.Set(i, s) + } + } + t.Assert(redBlackTree2.Size(), 5) + }) +} diff --git a/contrib/config/apollo/go.mod b/contrib/config/apollo/go.mod index 63f1444d3..4782834cf 100644 --- a/contrib/config/apollo/go.mod +++ b/contrib/config/apollo/go.mod @@ -4,7 +4,7 @@ go 1.23.0 require ( github.com/apolloconfig/agollo/v4 v4.3.1 - github.com/gogf/gf/v2 v2.9.7 + github.com/gogf/gf/v2 v2.9.8 ) require ( diff --git a/contrib/config/consul/go.mod b/contrib/config/consul/go.mod index 6c49d10fa..7c7003c95 100644 --- a/contrib/config/consul/go.mod +++ b/contrib/config/consul/go.mod @@ -3,7 +3,7 @@ module github.com/gogf/gf/contrib/config/consul/v2 go 1.23.0 require ( - github.com/gogf/gf/v2 v2.9.7 + github.com/gogf/gf/v2 v2.9.8 github.com/hashicorp/consul/api v1.24.0 github.com/hashicorp/go-cleanhttp v0.5.2 ) diff --git a/contrib/config/kubecm/go.mod b/contrib/config/kubecm/go.mod index cd972b980..243d4f33b 100644 --- a/contrib/config/kubecm/go.mod +++ b/contrib/config/kubecm/go.mod @@ -3,7 +3,7 @@ module github.com/gogf/gf/contrib/config/kubecm/v2 go 1.24.0 require ( - github.com/gogf/gf/v2 v2.9.7 + github.com/gogf/gf/v2 v2.9.8 k8s.io/api v0.33.4 k8s.io/apimachinery v0.33.4 k8s.io/client-go v0.33.4 diff --git a/contrib/config/nacos/go.mod b/contrib/config/nacos/go.mod index be67b6542..851f2a78e 100644 --- a/contrib/config/nacos/go.mod +++ b/contrib/config/nacos/go.mod @@ -3,7 +3,7 @@ module github.com/gogf/gf/contrib/config/nacos/v2 go 1.23.0 require ( - github.com/gogf/gf/v2 v2.9.7 + github.com/gogf/gf/v2 v2.9.8 github.com/nacos-group/nacos-sdk-go/v2 v2.3.3 ) diff --git a/contrib/config/polaris/go.mod b/contrib/config/polaris/go.mod index b8293ad31..316e98a12 100644 --- a/contrib/config/polaris/go.mod +++ b/contrib/config/polaris/go.mod @@ -3,7 +3,7 @@ module github.com/gogf/gf/contrib/config/polaris/v2 go 1.23.0 require ( - github.com/gogf/gf/v2 v2.9.7 + github.com/gogf/gf/v2 v2.9.8 github.com/polarismesh/polaris-go v1.6.1 ) diff --git a/contrib/drivers/clickhouse/go.mod b/contrib/drivers/clickhouse/go.mod index 30e9f374f..37940d8e5 100644 --- a/contrib/drivers/clickhouse/go.mod +++ b/contrib/drivers/clickhouse/go.mod @@ -4,7 +4,7 @@ go 1.23.0 require ( github.com/ClickHouse/clickhouse-go/v2 v2.0.15 - github.com/gogf/gf/v2 v2.9.7 + github.com/gogf/gf/v2 v2.9.8 github.com/google/uuid v1.6.0 github.com/shopspring/decimal v1.3.1 ) diff --git a/contrib/drivers/dm/go.mod b/contrib/drivers/dm/go.mod index 8ae3135ca..affe6ec4a 100644 --- a/contrib/drivers/dm/go.mod +++ b/contrib/drivers/dm/go.mod @@ -6,7 +6,7 @@ replace github.com/gogf/gf/v2 => ../../../ require ( gitee.com/chunanyong/dm v1.8.12 - github.com/gogf/gf/v2 v2.9.7 + github.com/gogf/gf/v2 v2.9.8 ) require ( diff --git a/contrib/drivers/gaussdb/gaussdb_convert.go b/contrib/drivers/gaussdb/gaussdb_convert.go index 27b292afe..67512f65f 100644 --- a/contrib/drivers/gaussdb/gaussdb_convert.go +++ b/contrib/drivers/gaussdb/gaussdb_convert.go @@ -11,8 +11,8 @@ import ( "reflect" "strings" + pq "gitee.com/opengauss/openGauss-connector-go-pq" "github.com/google/uuid" - "github.com/lib/pq" "github.com/gogf/gf/v2/database/gdb" "github.com/gogf/gf/v2/frame/g" diff --git a/contrib/drivers/gaussdb/gaussdb_open.go b/contrib/drivers/gaussdb/gaussdb_open.go index d1b5fc6d1..0ae6570c3 100644 --- a/contrib/drivers/gaussdb/gaussdb_open.go +++ b/contrib/drivers/gaussdb/gaussdb_open.go @@ -16,14 +16,14 @@ import ( "github.com/gogf/gf/v2/text/gstr" ) -// Open creates and returns an underlying sql.DB object for pgsql. -// https://pkg.go.dev/github.com/lib/pq +// Open creates and returns an underlying sql.DB object for GaussDB (openGauss). +// https://gitee.com/opengauss/openGauss-connector-go-pq func (d *Driver) Open(config *gdb.ConfigNode) (db *sql.DB, err error) { source, err := configNodeToSource(config) if err != nil { return nil, err } - underlyingDriverName := "postgres" + underlyingDriverName := "opengauss" if db, err = sql.Open(underlyingDriverName, source); err != nil { err = gerror.WrapCodef( gcode.CodeDbOperationError, err, diff --git a/contrib/drivers/gaussdb/go.mod b/contrib/drivers/gaussdb/go.mod index 634f3821f..2a6f906ea 100644 --- a/contrib/drivers/gaussdb/go.mod +++ b/contrib/drivers/gaussdb/go.mod @@ -4,9 +4,8 @@ go 1.23.0 require ( gitee.com/opengauss/openGauss-connector-go-pq v1.0.7 - github.com/gogf/gf/v2 v2.9.7 + github.com/gogf/gf/v2 v2.9.8 github.com/google/uuid v1.6.0 - github.com/lib/pq v1.10.9 ) require ( diff --git a/contrib/drivers/gaussdb/go.sum b/contrib/drivers/gaussdb/go.sum index 720a76750..d3266deaa 100644 --- a/contrib/drivers/gaussdb/go.sum +++ b/contrib/drivers/gaussdb/go.sum @@ -56,8 +56,6 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= -github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= diff --git a/contrib/drivers/mariadb/go.mod b/contrib/drivers/mariadb/go.mod index 061b9314c..2dc871cab 100644 --- a/contrib/drivers/mariadb/go.mod +++ b/contrib/drivers/mariadb/go.mod @@ -3,8 +3,8 @@ module github.com/gogf/gf/contrib/drivers/mariadb/v2 go 1.23.0 require ( - github.com/gogf/gf/contrib/drivers/mysql/v2 v2.9.7 - github.com/gogf/gf/v2 v2.9.7 + github.com/gogf/gf/contrib/drivers/mysql/v2 v2.9.8 + github.com/gogf/gf/v2 v2.9.8 ) require ( diff --git a/contrib/drivers/mssql/go.mod b/contrib/drivers/mssql/go.mod index 14efd3b55..e6f9c58f8 100644 --- a/contrib/drivers/mssql/go.mod +++ b/contrib/drivers/mssql/go.mod @@ -3,7 +3,7 @@ module github.com/gogf/gf/contrib/drivers/mssql/v2 go 1.23.0 require ( - github.com/gogf/gf/v2 v2.9.7 + github.com/gogf/gf/v2 v2.9.8 github.com/microsoft/go-mssqldb v1.7.1 ) diff --git a/contrib/drivers/mssql/mssql_z_unit_model_test.go b/contrib/drivers/mssql/mssql_z_unit_model_test.go index 1e49e5f5c..99ee09e8f 100644 --- a/contrib/drivers/mssql/mssql_z_unit_model_test.go +++ b/contrib/drivers/mssql/mssql_z_unit_model_test.go @@ -2359,7 +2359,12 @@ func Test_Model_Raw(t *testing.T) { OrderDesc("id"). Count() t.AssertNil(err) - t.Assert(count, int64(6)) + // After fix for issue #4500, Where conditions are correctly applied to Raw SQL Count. + // Raw SQL matches: id in (1, 5, 7, 8, 9, 10) + // WhereLT("id", 8): id < 8 -> (1, 5, 7) + // WhereIn("id", {1-7}): id in (1, 2, 3, 4, 5, 6, 7) -> (1, 5, 7) + // Result: 3 records match all conditions + t.Assert(count, int64(3)) }) } diff --git a/contrib/drivers/mysql/go.mod b/contrib/drivers/mysql/go.mod index b5ad4b86e..7ea870802 100644 --- a/contrib/drivers/mysql/go.mod +++ b/contrib/drivers/mysql/go.mod @@ -4,7 +4,7 @@ go 1.23.0 require ( github.com/go-sql-driver/mysql v1.7.1 - github.com/gogf/gf/v2 v2.9.7 + github.com/gogf/gf/v2 v2.9.8 ) require ( diff --git a/contrib/drivers/mysql/mysql_z_unit_issue_test.go b/contrib/drivers/mysql/mysql_z_unit_issue_test.go index 52c28b9fe..edb4b2a18 100644 --- a/contrib/drivers/mysql/mysql_z_unit_issue_test.go +++ b/contrib/drivers/mysql/mysql_z_unit_issue_test.go @@ -1865,3 +1865,87 @@ func Test_Issue4086(t *testing.T) { }) }) } + +// https://github.com/gogf/gf/issues/4500 +// Raw() Count ignores Where condition +func Test_Issue4500(t *testing.T) { + table := createInitTable() + defer dropTable(table) + + // Test 1: Raw SQL with WHERE + external Where condition + Count + // This tests that formatCondition correctly uses AND when Raw SQL already has WHERE + gtest.C(t, func(t *gtest.T) { + count, err := db. + Raw(fmt.Sprintf("SELECT * FROM %s WHERE id IN (?)", table), g.Slice{1, 5, 7, 8, 9, 10}). + WhereLT("id", 8). + Count() + t.AssertNil(err) + // Raw SQL: id IN (1,5,7,8,9,10) = 6 records + // Where: id < 8 filters to {1,5,7} = 3 records + t.Assert(count, 3) + }) + + // Test 2: Raw SQL without WHERE + external Where condition + Count + // This tests that formatCondition correctly adds WHERE + gtest.C(t, func(t *gtest.T) { + count, err := db. + Raw(fmt.Sprintf("SELECT * FROM %s", table)). + WhereLT("id", 5). + Count() + t.AssertNil(err) + // Raw SQL: all 10 records + // Where: id < 5 = {1,2,3,4} = 4 records + t.Assert(count, 4) + }) + + // Test 3: Raw + Where + ScanAndCount + gtest.C(t, func(t *gtest.T) { + type User struct { + Id int + Passport string + } + var users []User + var total int + err := db. + Raw(fmt.Sprintf("SELECT * FROM %s WHERE id IN (?)", table), g.Slice{1, 5, 7, 8, 9, 10}). + WhereLT("id", 8). + ScanAndCount(&users, &total, false) + t.AssertNil(err) + // Both scan result and count should respect Where condition + t.Assert(len(users), 3) + t.Assert(total, 3) + }) + + // Test 4: Raw + multiple Where conditions + Count + gtest.C(t, func(t *gtest.T) { + count, err := db. + Raw(fmt.Sprintf("SELECT * FROM %s WHERE id > ?", table), 0). + WhereLT("id", 5). + WhereGTE("id", 2). + Count() + t.AssertNil(err) + // Raw: id > 0 (all 10 records) + // Where: id < 5 AND id >= 2 = {2, 3, 4} = 3 records + t.Assert(count, 3) + }) + + // Test 5: Raw SQL with no external Where + Count (baseline test) + gtest.C(t, func(t *gtest.T) { + count, err := db. + Raw(fmt.Sprintf("SELECT * FROM %s WHERE id IN (?)", table), g.Slice{1, 2, 3}). + Count() + t.AssertNil(err) + // Should count 3 records + t.Assert(count, 3) + }) + + // Test 6: Verify All() still works correctly with Raw + Where + gtest.C(t, func(t *gtest.T) { + all, err := db. + Raw(fmt.Sprintf("SELECT * FROM %s WHERE id IN (?)", table), g.Slice{1, 5, 7, 8, 9, 10}). + WhereLT("id", 8). + All() + t.AssertNil(err) + t.Assert(len(all), 3) + }) +} diff --git a/contrib/drivers/mysql/mysql_z_unit_model_test.go b/contrib/drivers/mysql/mysql_z_unit_model_test.go index a19ef4d35..cd56d5d16 100644 --- a/contrib/drivers/mysql/mysql_z_unit_model_test.go +++ b/contrib/drivers/mysql/mysql_z_unit_model_test.go @@ -2944,7 +2944,9 @@ func Test_Model_Raw(t *testing.T) { Limit(2). Count() t.AssertNil(err) - t.Assert(count, int64(6)) + // Raw SQL selects {1,5,7,8,9,10}, Where filters to id < 8 AND id IN {1,2,3,4,5,6,7} + // Result: {1,5,7} = 3 records + t.Assert(count, int64(3)) }) } diff --git a/contrib/drivers/oceanbase/go.mod b/contrib/drivers/oceanbase/go.mod index 89ddc9907..98a192c0d 100644 --- a/contrib/drivers/oceanbase/go.mod +++ b/contrib/drivers/oceanbase/go.mod @@ -3,8 +3,8 @@ module github.com/gogf/gf/contrib/drivers/oceanbase/v2 go 1.23.0 require ( - github.com/gogf/gf/contrib/drivers/mysql/v2 v2.9.7 - github.com/gogf/gf/v2 v2.9.7 + github.com/gogf/gf/contrib/drivers/mysql/v2 v2.9.8 + github.com/gogf/gf/v2 v2.9.8 ) require ( diff --git a/contrib/drivers/oracle/go.mod b/contrib/drivers/oracle/go.mod index 303a674b6..57966aca3 100644 --- a/contrib/drivers/oracle/go.mod +++ b/contrib/drivers/oracle/go.mod @@ -3,7 +3,7 @@ module github.com/gogf/gf/contrib/drivers/oracle/v2 go 1.23.0 require ( - github.com/gogf/gf/v2 v2.9.7 + github.com/gogf/gf/v2 v2.9.8 github.com/sijms/go-ora/v2 v2.7.10 ) diff --git a/contrib/drivers/oracle/oracle_z_unit_model_test.go b/contrib/drivers/oracle/oracle_z_unit_model_test.go index 185446b24..f10e91eab 100644 --- a/contrib/drivers/oracle/oracle_z_unit_model_test.go +++ b/contrib/drivers/oracle/oracle_z_unit_model_test.go @@ -1347,7 +1347,12 @@ func Test_Model_Raw(t *testing.T) { OrderDesc("ID"). Count() t.AssertNil(err) - t.Assert(count, 6) + // After fix for issue #4500, Where conditions are correctly applied to Raw SQL Count. + // Raw SQL matches: id in (1, 5, 7, 8, 9, 10) + // WhereLT("ID", 8): id < 8 -> (1, 5, 7) + // WhereIn("ID", {1-7}): id in (1, 2, 3, 4, 5, 6, 7) -> (1, 5, 7) + // Result: 3 records match all conditions + t.Assert(count, int64(3)) }) } diff --git a/contrib/drivers/pgsql/go.mod b/contrib/drivers/pgsql/go.mod index f36acfce7..6803cf891 100644 --- a/contrib/drivers/pgsql/go.mod +++ b/contrib/drivers/pgsql/go.mod @@ -3,7 +3,7 @@ module github.com/gogf/gf/contrib/drivers/pgsql/v2 go 1.23.0 require ( - github.com/gogf/gf/v2 v2.9.7 + github.com/gogf/gf/v2 v2.9.8 github.com/google/uuid v1.6.0 github.com/lib/pq v1.10.9 ) diff --git a/contrib/drivers/pgsql/pgsql_z_unit_issue_test.go b/contrib/drivers/pgsql/pgsql_z_unit_issue_test.go index 6fd5817fa..0e706916d 100644 --- a/contrib/drivers/pgsql/pgsql_z_unit_issue_test.go +++ b/contrib/drivers/pgsql/pgsql_z_unit_issue_test.go @@ -205,3 +205,178 @@ func Test_Issue4033(t *testing.T) { t.AssertNil(err) }) } + +// https://github.com/gogf/gf/issues/4500 +// Raw() Count ignores Where condition +func Test_Issue4500(t *testing.T) { + table := createInitTable() + defer dropTable(table) + + // Test 1: Raw SQL with WHERE + external Where condition + Count + // This tests that formatCondition correctly uses AND when Raw SQL already has WHERE + gtest.C(t, func(t *gtest.T) { + count, err := db. + Raw(fmt.Sprintf("SELECT * FROM %s WHERE id IN (?)", table), g.Slice{1, 5, 7, 8, 9, 10}). + WhereLT("id", 8). + Count() + t.AssertNil(err) + // Raw SQL: id IN (1,5,7,8,9,10) = 6 records + // Where: id < 8 filters to {1,5,7} = 3 records + t.Assert(count, 3) + }) + + // Test 2: Raw SQL without WHERE + external Where condition + Count + // This tests that formatCondition correctly adds WHERE + gtest.C(t, func(t *gtest.T) { + count, err := db. + Raw(fmt.Sprintf("SELECT * FROM %s", table)). + WhereLT("id", 5). + Count() + t.AssertNil(err) + // Raw SQL: all 10 records + // Where: id < 5 = {1,2,3,4} = 4 records + t.Assert(count, 4) + }) + + // Test 3: Raw + Where + ScanAndCount + gtest.C(t, func(t *gtest.T) { + type User struct { + Id int + Passport string + } + var users []User + var total int + err := db. + Raw(fmt.Sprintf("SELECT * FROM %s WHERE id IN (?)", table), g.Slice{1, 5, 7, 8, 9, 10}). + WhereLT("id", 8). + ScanAndCount(&users, &total, false) + t.AssertNil(err) + // Both scan result and count should respect Where condition + t.Assert(len(users), 3) + t.Assert(total, 3) + }) + + // Test 4: Raw + multiple Where conditions + Count + gtest.C(t, func(t *gtest.T) { + count, err := db. + Raw(fmt.Sprintf("SELECT * FROM %s WHERE id > ?", table), 0). + WhereLT("id", 5). + WhereGTE("id", 2). + Count() + t.AssertNil(err) + // Raw: id > 0 (all 10 records) + // Where: id < 5 AND id >= 2 = {2, 3, 4} = 3 records + t.Assert(count, 3) + }) + + // Test 5: Raw SQL with no external Where + Count (baseline test) + gtest.C(t, func(t *gtest.T) { + count, err := db. + Raw(fmt.Sprintf("SELECT * FROM %s WHERE id IN (?)", table), g.Slice{1, 2, 3}). + Count() + t.AssertNil(err) + // Should count 3 records + t.Assert(count, 3) + }) + + // Test 6: Verify All() still works correctly with Raw + Where + gtest.C(t, func(t *gtest.T) { + all, err := db. + Raw(fmt.Sprintf("SELECT * FROM %s WHERE id IN (?)", table), g.Slice{1, 5, 7, 8, 9, 10}). + WhereLT("id", 8). + All() + t.AssertNil(err) + t.Assert(len(all), 3) + }) +} + +// https://github.com/gogf/gf/issues/4595 +// FieldsPrefix silently drops fields when using table alias before LeftJoin. +func Test_Issue4595(t *testing.T) { + var ( + tableUser = fmt.Sprintf(`%s_%d`, TablePrefix+"issue4595_user", gtime.TimestampNano()) + tableUserDetail = fmt.Sprintf(`%s_%d`, TablePrefix+"issue4595_user_detail", gtime.TimestampNano()) + ) + + // Create user table + if _, err := db.Exec(ctx, fmt.Sprintf(` + CREATE TABLE %s ( + id bigserial PRIMARY KEY, + name varchar(100), + email varchar(100) + );`, tableUser, + )); err != nil { + gtest.Fatal(err) + } + defer dropTable(tableUser) + + // Create user_detail table + if _, err := db.Exec(ctx, fmt.Sprintf(` + CREATE TABLE %s ( + id bigserial PRIMARY KEY, + user_id bigint, + phone varchar(20), + address varchar(200) + );`, tableUserDetail, + )); err != nil { + gtest.Fatal(err) + } + defer dropTable(tableUserDetail) + + // Insert test data + if _, err := db.Exec(ctx, fmt.Sprintf(` + INSERT INTO %s (id, name, email) VALUES (1, 'john', 'john@example.com'); + INSERT INTO %s (id, user_id, phone, address) VALUES (1, 1, '1234567890', '123 Main St'); + `, tableUser, tableUserDetail)); err != nil { + gtest.Fatal(err) + } + + gtest.C(t, func(t *gtest.T) { + // Test case 1: FieldsPrefix called before LeftJoin + // Both t1 and t2 fields should be present + r, err := db.Model(tableUser).As("t1"). + FieldsPrefix("t2", "phone", "address"). + FieldsPrefix("t1", "id", "name", "email"). + LeftJoin(tableUserDetail, "t2", "t1.id=t2.user_id"). + All() + + t.AssertNil(err) + t.Assert(len(r), 1) + t.Assert(r[0]["id"], 1) + t.Assert(r[0]["name"], "john") + t.Assert(r[0]["email"], "john@example.com") + t.Assert(r[0]["phone"], "1234567890") + t.Assert(r[0]["address"], "123 Main St") + }) + + gtest.C(t, func(t *gtest.T) { + // Test case 2: Using Fields() with prefix + r, err := db.Model(tableUser).As("t1"). + Fields("t2.phone", "t2.address", "t1.id", "t1.name", "t1.email"). + LeftJoin(tableUserDetail, "t2", "t1.id=t2.user_id"). + All() + t.AssertNil(err) + t.Assert(len(r), 1) + t.Assert(r[0]["id"], 1) + t.Assert(r[0]["name"], "john") + t.Assert(r[0]["email"], "john@example.com") + t.Assert(r[0]["phone"], "1234567890") + t.Assert(r[0]["address"], "123 Main St") + }) + + gtest.C(t, func(t *gtest.T) { + // Test case 3: FieldsPrefix called after LeftJoin + r, err := db.Model(tableUser).As("t1"). + LeftJoin(tableUserDetail, "t2", "t1.id=t2.user_id"). + FieldsPrefix("t2", "phone", "address"). + FieldsPrefix("t1", "id", "name", "email"). + All() + t.AssertNil(err) + t.Assert(len(r), 1) + t.Assert(r[0]["id"], 1) + t.Assert(r[0]["name"], "john") + t.Assert(r[0]["email"], "john@example.com") + t.Assert(r[0]["phone"], "1234567890") + t.Assert(r[0]["address"], "123 Main St") + }) +} diff --git a/contrib/drivers/sqlite/go.mod b/contrib/drivers/sqlite/go.mod index 7d58450a3..a30cd83b1 100644 --- a/contrib/drivers/sqlite/go.mod +++ b/contrib/drivers/sqlite/go.mod @@ -4,7 +4,7 @@ go 1.23.0 require ( github.com/glebarez/go-sqlite v1.21.2 - github.com/gogf/gf/v2 v2.9.7 + github.com/gogf/gf/v2 v2.9.8 ) require ( diff --git a/contrib/drivers/sqlite/sqlite_z_unit_model_test.go b/contrib/drivers/sqlite/sqlite_z_unit_model_test.go index 03e8465c7..a6ec678f4 100644 --- a/contrib/drivers/sqlite/sqlite_z_unit_model_test.go +++ b/contrib/drivers/sqlite/sqlite_z_unit_model_test.go @@ -3482,7 +3482,12 @@ func Test_Model_Raw(t *testing.T) { Limit(2). Count() t.AssertNil(err) - t.Assert(count, int64(6)) + // After fix for issue #4500, Where conditions are correctly applied to Raw SQL Count. + // Raw SQL matches: id in (1, 5, 7, 8, 9, 10) + // WhereLT("id", 8): id < 8 -> (1, 5, 7) + // WhereIn("id", {1-7}): id in (1, 2, 3, 4, 5, 6, 7) -> (1, 5, 7) + // Result: 3 records match all conditions + t.Assert(count, int64(3)) }) } diff --git a/contrib/drivers/sqlitecgo/go.mod b/contrib/drivers/sqlitecgo/go.mod index b576b5a8d..c95dfc713 100644 --- a/contrib/drivers/sqlitecgo/go.mod +++ b/contrib/drivers/sqlitecgo/go.mod @@ -3,7 +3,7 @@ module github.com/gogf/gf/contrib/drivers/sqlitecgo/v2 go 1.23.0 require ( - github.com/gogf/gf/v2 v2.9.7 + github.com/gogf/gf/v2 v2.9.8 github.com/mattn/go-sqlite3 v1.14.17 ) diff --git a/contrib/drivers/sqlitecgo/sqlitecgo_z_unit_model_test.go b/contrib/drivers/sqlitecgo/sqlitecgo_z_unit_model_test.go index 3ad793df2..7eed117d5 100644 --- a/contrib/drivers/sqlitecgo/sqlitecgo_z_unit_model_test.go +++ b/contrib/drivers/sqlitecgo/sqlitecgo_z_unit_model_test.go @@ -3480,7 +3480,12 @@ func Test_Model_Raw(t *testing.T) { Limit(2). Count() t.AssertNil(err) - t.Assert(count, int64(6)) + // After fix for issue #4500, Where conditions are correctly applied to Raw SQL Count. + // Raw SQL matches: id in (1, 5, 7, 8, 9, 10) + // WhereLT("id", 8): id < 8 -> (1, 5, 7) + // WhereIn("id", {1-7}): id in (1, 2, 3, 4, 5, 6, 7) -> (1, 5, 7) + // Result: 3 records match all conditions + t.Assert(count, int64(3)) }) } diff --git a/contrib/drivers/tidb/go.mod b/contrib/drivers/tidb/go.mod index 29bc2493a..d99277e80 100644 --- a/contrib/drivers/tidb/go.mod +++ b/contrib/drivers/tidb/go.mod @@ -3,8 +3,8 @@ module github.com/gogf/gf/contrib/drivers/tidb/v2 go 1.23.0 require ( - github.com/gogf/gf/contrib/drivers/mysql/v2 v2.9.7 - github.com/gogf/gf/v2 v2.9.7 + github.com/gogf/gf/contrib/drivers/mysql/v2 v2.9.8 + github.com/gogf/gf/v2 v2.9.8 ) require ( diff --git a/contrib/metric/otelmetric/go.mod b/contrib/metric/otelmetric/go.mod index 47f77f523..cac8ccb97 100644 --- a/contrib/metric/otelmetric/go.mod +++ b/contrib/metric/otelmetric/go.mod @@ -3,7 +3,7 @@ module github.com/gogf/gf/contrib/metric/otelmetric/v2 go 1.23.0 require ( - github.com/gogf/gf/v2 v2.9.7 + github.com/gogf/gf/v2 v2.9.8 github.com/prometheus/client_golang v1.23.2 go.opentelemetry.io/contrib/instrumentation/runtime v0.63.0 go.opentelemetry.io/otel v1.38.0 diff --git a/contrib/nosql/redis/go.mod b/contrib/nosql/redis/go.mod index 927d5de39..86136d113 100644 --- a/contrib/nosql/redis/go.mod +++ b/contrib/nosql/redis/go.mod @@ -3,7 +3,7 @@ module github.com/gogf/gf/contrib/nosql/redis/v2 go 1.23.0 require ( - github.com/gogf/gf/v2 v2.9.7 + github.com/gogf/gf/v2 v2.9.8 github.com/redis/go-redis/v9 v9.12.1 go.opentelemetry.io/otel v1.38.0 go.opentelemetry.io/otel/trace v1.38.0 diff --git a/contrib/registry/consul/go.mod b/contrib/registry/consul/go.mod index 571ff3f2c..54dc594cd 100644 --- a/contrib/registry/consul/go.mod +++ b/contrib/registry/consul/go.mod @@ -3,7 +3,7 @@ module github.com/gogf/gf/contrib/registry/consul/v2 go 1.23.0 require ( - github.com/gogf/gf/v2 v2.9.7 + github.com/gogf/gf/v2 v2.9.8 github.com/hashicorp/consul/api v1.26.1 ) diff --git a/contrib/registry/etcd/go.mod b/contrib/registry/etcd/go.mod index 56fe7b5db..19c03a35e 100644 --- a/contrib/registry/etcd/go.mod +++ b/contrib/registry/etcd/go.mod @@ -3,7 +3,7 @@ module github.com/gogf/gf/contrib/registry/etcd/v2 go 1.23.0 require ( - github.com/gogf/gf/v2 v2.9.7 + github.com/gogf/gf/v2 v2.9.8 go.etcd.io/etcd/client/v3 v3.5.17 google.golang.org/grpc v1.59.0 ) diff --git a/contrib/registry/file/go.mod b/contrib/registry/file/go.mod index 38b88d491..e30d05b7f 100644 --- a/contrib/registry/file/go.mod +++ b/contrib/registry/file/go.mod @@ -2,7 +2,7 @@ module github.com/gogf/gf/contrib/registry/file/v2 go 1.23.0 -require github.com/gogf/gf/v2 v2.9.7 +require github.com/gogf/gf/v2 v2.9.8 require ( github.com/BurntSushi/toml v1.5.0 // indirect diff --git a/contrib/registry/nacos/go.mod b/contrib/registry/nacos/go.mod index 9599ee4a5..48c7e198e 100644 --- a/contrib/registry/nacos/go.mod +++ b/contrib/registry/nacos/go.mod @@ -3,7 +3,7 @@ module github.com/gogf/gf/contrib/registry/nacos/v2 go 1.23.0 require ( - github.com/gogf/gf/v2 v2.9.7 + github.com/gogf/gf/v2 v2.9.8 github.com/nacos-group/nacos-sdk-go/v2 v2.3.3 ) diff --git a/contrib/registry/nacos/nacos.go b/contrib/registry/nacos/nacos.go index 799db2a5d..9ecdd3a6a 100644 --- a/contrib/registry/nacos/nacos.go +++ b/contrib/registry/nacos/nacos.go @@ -34,9 +34,11 @@ var ( // Registry is nacos registry. type Registry struct { - client naming_client.INamingClient - clusterName string - groupName string + client naming_client.INamingClient + clusterName string + groupName string + defaultEndpoint string + defaultMetadata map[string]string } // Config is the configuration object for nacos client. @@ -101,9 +103,10 @@ func NewWithConfig(ctx context.Context, config Config) (reg *Registry, err error // NewWithClient new the instance with INamingClient func NewWithClient(client naming_client.INamingClient) *Registry { r := &Registry{ - client: client, - clusterName: "DEFAULT", - groupName: "DEFAULT_GROUP", + client: client, + clusterName: "DEFAULT", + groupName: "DEFAULT_GROUP", + defaultMetadata: make(map[string]string), } return r } @@ -119,3 +122,17 @@ func (reg *Registry) SetGroupName(groupName string) *Registry { reg.groupName = groupName return reg } + +// SetDefaultEndpoint sets the default endpoint for service registration. +// It overrides the service endpoints when registering if it's not empty. +func (reg *Registry) SetDefaultEndpoint(endpoint string) *Registry { + reg.defaultEndpoint = endpoint + return reg +} + +// SetDefaultMetadata sets the default metadata for service registration. +// It will be merged with service's original metadata when registering. +func (reg *Registry) SetDefaultMetadata(metadata map[string]string) *Registry { + reg.defaultMetadata = metadata + return reg +} diff --git a/contrib/registry/nacos/nacos_register.go b/contrib/registry/nacos/nacos_register.go index 0b94c97ce..c4c8ceddb 100644 --- a/contrib/registry/nacos/nacos_register.go +++ b/contrib/registry/nacos/nacos_register.go @@ -20,16 +20,28 @@ import ( func (reg *Registry) Register(ctx context.Context, service gsvc.Service) (registered gsvc.Service, err error) { metadata := map[string]string{} endpoints := service.GetEndpoints() + + // Apply default endpoint override if configured + if reg.defaultEndpoint != "" { + endpoints = gsvc.Endpoints{gsvc.NewEndpoint(reg.defaultEndpoint)} + } + p := vo.BatchRegisterInstanceParam{ ServiceName: service.GetName(), GroupName: reg.groupName, Instances: make([]vo.RegisterInstanceParam, 0, len(endpoints)), } + // Copy service metadata for k, v := range service.GetMetadata() { metadata[k] = gconv.String(v) } + // Apply default metadata if configured + for k, v := range reg.defaultMetadata { + metadata[k] = v + } + for _, endpoint := range endpoints { p.Instances = append(p.Instances, vo.RegisterInstanceParam{ Ip: endpoint.Host(), diff --git a/contrib/registry/polaris/go.mod b/contrib/registry/polaris/go.mod index 23a173719..2d1936bef 100644 --- a/contrib/registry/polaris/go.mod +++ b/contrib/registry/polaris/go.mod @@ -3,7 +3,7 @@ module github.com/gogf/gf/contrib/registry/polaris/v2 go 1.23.0 require ( - github.com/gogf/gf/v2 v2.9.7 + github.com/gogf/gf/v2 v2.9.8 github.com/polarismesh/polaris-go v1.6.1 ) diff --git a/contrib/registry/zookeeper/go.mod b/contrib/registry/zookeeper/go.mod index fa72ac1d0..ec609970a 100644 --- a/contrib/registry/zookeeper/go.mod +++ b/contrib/registry/zookeeper/go.mod @@ -4,7 +4,7 @@ go 1.23.0 require ( github.com/go-zookeeper/zk v1.0.3 - github.com/gogf/gf/v2 v2.9.7 + github.com/gogf/gf/v2 v2.9.8 golang.org/x/sync v0.16.0 ) diff --git a/contrib/rpc/grpcx/go.mod b/contrib/rpc/grpcx/go.mod index 9d6bb74ff..93df1b77d 100644 --- a/contrib/rpc/grpcx/go.mod +++ b/contrib/rpc/grpcx/go.mod @@ -3,8 +3,8 @@ module github.com/gogf/gf/contrib/rpc/grpcx/v2 go 1.23.0 require ( - github.com/gogf/gf/contrib/registry/file/v2 v2.9.7 - github.com/gogf/gf/v2 v2.9.7 + github.com/gogf/gf/contrib/registry/file/v2 v2.9.8 + github.com/gogf/gf/v2 v2.9.8 go.opentelemetry.io/otel v1.38.0 go.opentelemetry.io/otel/trace v1.38.0 google.golang.org/grpc v1.64.1 diff --git a/contrib/sdk/httpclient/go.mod b/contrib/sdk/httpclient/go.mod index 4b48c11f6..87276699f 100644 --- a/contrib/sdk/httpclient/go.mod +++ b/contrib/sdk/httpclient/go.mod @@ -2,7 +2,7 @@ module github.com/gogf/gf/contrib/sdk/httpclient/v2 go 1.23.0 -require github.com/gogf/gf/v2 v2.9.7 +require github.com/gogf/gf/v2 v2.9.8 require ( github.com/BurntSushi/toml v1.5.0 // indirect diff --git a/contrib/trace/otlpgrpc/go.mod b/contrib/trace/otlpgrpc/go.mod index 441771ab6..ff176b4bf 100644 --- a/contrib/trace/otlpgrpc/go.mod +++ b/contrib/trace/otlpgrpc/go.mod @@ -3,7 +3,7 @@ module github.com/gogf/gf/contrib/trace/otlpgrpc/v2 go 1.23.0 require ( - github.com/gogf/gf/v2 v2.9.7 + github.com/gogf/gf/v2 v2.9.8 go.opentelemetry.io/otel v1.38.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 diff --git a/contrib/trace/otlphttp/go.mod b/contrib/trace/otlphttp/go.mod index 43eb7105f..814957e2f 100644 --- a/contrib/trace/otlphttp/go.mod +++ b/contrib/trace/otlphttp/go.mod @@ -3,7 +3,7 @@ module github.com/gogf/gf/contrib/trace/otlphttp/v2 go 1.23.0 require ( - github.com/gogf/gf/v2 v2.9.7 + github.com/gogf/gf/v2 v2.9.8 go.opentelemetry.io/otel v1.38.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 diff --git a/database/gdb/gdb.go b/database/gdb/gdb.go index f21b5f307..9c3bd7f81 100644 --- a/database/gdb/gdb.go +++ b/database/gdb/gdb.go @@ -294,6 +294,9 @@ type DB interface { // SetMaxConnLifeTime sets the maximum amount of time a connection may be reused. SetMaxConnLifeTime(d time.Duration) + // SetMaxIdleConnTime sets the maximum amount of time a connection may be idle before being closed. + SetMaxIdleConnTime(d time.Duration) + // =========================================================================== // Utility methods. // =========================================================================== @@ -528,6 +531,7 @@ type dynamicConfig struct { MaxIdleConnCount int MaxOpenConnCount int MaxConnLifeTime time.Duration + MaxIdleConnTime time.Duration } // DoCommitInput is the input parameters for function DoCommit. @@ -863,8 +867,10 @@ const ( ) var ( + // checker is the checker function for instances map. + checker = func(v DB) bool { return v == nil } // instances is the management map for instances. - instances = gmap.NewStrAnyMap(true) + instances = gmap.NewKVMapWithChecker[string, DB](checker, true) // driverMap manages all custom registered driver. driverMap = map[string]Driver{} @@ -963,6 +969,7 @@ func newDBByConfigNode(node *ConfigNode, group string) (db DB, err error) { MaxIdleConnCount: node.MaxIdleConnCount, MaxOpenConnCount: node.MaxOpenConnCount, MaxConnLifeTime: node.MaxConnLifeTime, + MaxIdleConnTime: node.MaxIdleConnTime, }, } if v, ok := driverMap[node.Type]; ok { @@ -985,14 +992,14 @@ func Instance(name ...string) (db DB, err error) { if len(name) > 0 && name[0] != "" { group = name[0] } - v := instances.GetOrSetFuncLock(group, func() any { + v := instances.GetOrSetFuncLock(group, func() DB { db, err = NewByGroup(group) return db }) if v != nil { - return v.(DB), nil + return v, nil } - return + return nil, err } // getConfigNodeByGroup calculates and returns a configuration node of given group. It @@ -1142,6 +1149,9 @@ func (c *Core) getSqlDb(master bool, schema ...string) (sqlDb *sql.DB, err error } else { sqlDb.SetConnMaxLifetime(defaultMaxConnLifeTime) } + if c.dynamicConfig.MaxIdleConnTime > 0 { + sqlDb.SetConnMaxIdleTime(c.dynamicConfig.MaxIdleConnTime) + } return sqlDb } // it here uses NODE VALUE not pointer as the cache key, in case of oracle ORA-12516 error. diff --git a/database/gdb/gdb_core_config.go b/database/gdb/gdb_core_config.go index 7d18da91f..b051594bd 100644 --- a/database/gdb/gdb_core_config.go +++ b/database/gdb/gdb_core_config.go @@ -108,6 +108,11 @@ type ConfigNode struct { // Optional field MaxConnLifeTime time.Duration `json:"maxLifeTime"` + // MaxIdleConnTime specifies the maximum idle time of a connection before being closed + // This is Go 1.15+ feature: sql.DB.SetConnMaxIdleTime + // Optional field + MaxIdleConnTime time.Duration `json:"maxIdleTime"` + // QueryTimeout specifies the maximum execution time for DQL operations // Optional field QueryTimeout time.Duration `json:"queryTimeout"` @@ -353,6 +358,16 @@ func (c *Core) SetMaxConnLifeTime(d time.Duration) { c.dynamicConfig.MaxConnLifeTime = d } +// SetMaxIdleConnTime sets the maximum amount of time a connection may be idle before being closed. +// +// Idle connections may be closed lazily before reuse. +// +// If d <= 0, connections are not closed due to a connection's idle time. +// This is Go 1.15+ feature: sql.DB.SetConnMaxIdleTime. +func (c *Core) SetMaxIdleConnTime(d time.Duration) { + c.dynamicConfig.MaxIdleConnTime = d +} + // GetConfig returns the current used node configuration. func (c *Core) GetConfig() *ConfigNode { var configNode = c.getConfigNodeFromCtx(c.db.GetCtx()) diff --git a/database/gdb/gdb_core_underlying.go b/database/gdb/gdb_core_underlying.go index 0f78e25f6..21d8da0e9 100644 --- a/database/gdb/gdb_core_underlying.go +++ b/database/gdb/gdb_core_underlying.go @@ -166,6 +166,19 @@ func (c *Core) DoCommit(ctx context.Context, in DoCommitInput) (out DoCommitOutp timestampMilli1 = gtime.TimestampMilli() ) + // Panic recovery to handle panics from underlying database drivers + defer func() { + if exception := recover(); exception != nil { + if err == nil { + if v, ok := exception.(error); ok && gerror.HasStack(v) { + err = v + } else { + err = gerror.WrapCodef(gcode.CodeDbOperationError, gerror.NewCodef(gcode.CodeInternalPanic, "%+v", exception), FormatSqlWithArgs(in.Sql, in.Args)) + } + } + } + }() + // Trace span start. tr := otel.GetTracerProvider().Tracer(traceInstrumentName, trace.WithInstrumentationVersion(gf.VERSION)) ctx, span := tr.Start(ctx, string(in.Type), trace.WithSpanKind(trace.SpanKindClient)) diff --git a/database/gdb/gdb_func.go b/database/gdb/gdb_func.go index 29b853756..437c981f8 100644 --- a/database/gdb/gdb_func.go +++ b/database/gdb/gdb_func.go @@ -151,13 +151,26 @@ func isDoStruct(object any) bool { // getTableNameFromOrmTag retrieves and returns the table name from struct object. func getTableNameFromOrmTag(object any) string { var tableName string - // Use the interface value. - if r, ok := object.(iTableName); ok { - tableName = r.TableName() + var actualObj = object + + if rv, ok := object.(reflect.Value); ok { + // Check if reflect.Value is valid + if rv.IsValid() && rv.CanInterface() { + actualObj = rv.Interface() + } else { + // If reflect.Value is invalid, we cannot proceed with interface checks + return "" + } } - // User meta data tag "orm". - if tableName == "" { - if ormTag := gmeta.Get(object, OrmTagForStruct); !ormTag.IsEmpty() { + + // Check iTableName interface + if actualObj != nil { + if r, ok := actualObj.(iTableName); ok { + return r.TableName() + } + + // User meta data tag "orm". + if ormTag := gmeta.Get(actualObj, OrmTagForStruct); !ormTag.IsEmpty() { match, _ := gregex.MatchString( fmt.Sprintf(`%s\s*:\s*([^,]+)`, OrmTagForTable), ormTag.String(), @@ -166,17 +179,19 @@ func getTableNameFromOrmTag(object any) string { tableName = match[1] } } - } - // Use the struct name of snake case. - if tableName == "" { - if t, err := gstructs.StructType(object); err != nil { - panic(err) - } else { - tableName = gstr.CaseSnakeFirstUpper( - gstr.StrEx(t.String(), "."), - ) + + // Use the struct name of snake case. + if tableName == "" { + if t, err := gstructs.StructType(actualObj); err != nil { + panic(err) + } else { + tableName = gstr.CaseSnakeFirstUpper( + gstr.StrEx(t.String(), "."), + ) + } } } + return tableName } diff --git a/database/gdb/gdb_model.go b/database/gdb/gdb_model.go index f02ab4c08..39b9fed35 100644 --- a/database/gdb/gdb_model.go +++ b/database/gdb/gdb_model.go @@ -17,44 +17,45 @@ import ( // Model is core struct implementing the DAO for ORM. type Model struct { - db DB // Underlying DB interface. - tx TX // Underlying TX interface. - rawSql string // rawSql is the raw SQL string which marks a raw SQL based Model not a table based Model. - schema string // Custom database schema. - linkType int // Mark for operation on master or slave. - tablesInit string // Table names when model initialization. - tables string // Operation table names, which can be more than one table names and aliases, like: "user", "user u", "user u, user_detail ud". - fields []any // Operation fields, multiple fields joined using char ','. - fieldsEx []any // Excluded operation fields, it here uses slice instead of string type for quick filtering. - withArray []any // Arguments for With feature. - withAll bool // Enable model association operations on all objects that have "with" tag in the struct. - extraArgs []any // Extra custom arguments for sql, which are prepended to the arguments before sql committed to underlying driver. - whereBuilder *WhereBuilder // Condition builder for where operation. - groupBy string // Used for "group by" statement. - orderBy string // Used for "order by" statement. - having []any // Used for "having..." statement. - start int // Used for "select ... start, limit ..." statement. - limit int // Used for "select ... start, limit ..." statement. - option int // Option for extra operation features. - offset int // Offset statement for some databases grammar. - partition string // Partition table partition name. - data any // Data for operation, which can be type of map/[]map/struct/*struct/string, etc. - batch int // Batch number for batch Insert/Replace/Save operations. - filter bool // Filter data and where key-value pairs according to the fields of the table. - distinct string // Force the query to only return distinct results. - lockInfo string // Lock for update or in shared lock. - cacheEnabled bool // Enable sql result cache feature, which is mainly for indicating cache duration(especially 0) usage. - cacheOption CacheOption // Cache option for query statement. - hookHandler HookHandler // Hook functions for model hook feature. - unscoped bool // Disables soft deleting features when select/delete operations. - safe bool // If true, it clones and returns a new model object whenever operation done; or else it changes the attribute of current model. - onDuplicate any // onDuplicate is used for on Upsert clause. - onDuplicateEx any // onDuplicateEx is used for excluding some columns on Upsert clause. - onConflict any // onConflict is used for conflict keys on Upsert clause. - tableAliasMap map[string]string // Table alias to true table name, usually used in join statements. - softTimeOption SoftTimeOption // SoftTimeOption is the option to customize soft time feature for Model. - shardingConfig ShardingConfig // ShardingConfig for database/table sharding feature. - shardingValue any // Sharding value for sharding feature. + db DB // Underlying DB interface. + tx TX // Underlying TX interface. + rawSql string // rawSql is the raw SQL string which marks a raw SQL based Model not a table based Model. + schema string // Custom database schema. + linkType int // Mark for operation on master or slave. + tablesInit string // Table names when model initialization. + tables string // Operation table names, which can be more than one table names and aliases, like: "user", "user u", "user u, user_detail ud". + fields []any // Operation fields, multiple fields joined using char ','. + fieldsEx []any // Excluded operation fields, it here uses slice instead of string type for quick filtering. + withArray []any // Arguments for With feature. + withAll bool // Enable model association operations on all objects that have "with" tag in the struct. + extraArgs []any // Extra custom arguments for sql, which are prepended to the arguments before sql committed to underlying driver. + whereBuilder *WhereBuilder // Condition builder for where operation. + groupBy string // Used for "group by" statement. + orderBy string // Used for "order by" statement. + having []any // Used for "having..." statement. + start int // Used for "select ... start, limit ..." statement. + limit int // Used for "select ... start, limit ..." statement. + option int // Option for extra operation features. + offset int // Offset statement for some databases grammar. + partition string // Partition table partition name. + data any // Data for operation, which can be type of map/[]map/struct/*struct/string, etc. + batch int // Batch number for batch Insert/Replace/Save operations. + filter bool // Filter data and where key-value pairs according to the fields of the table. + distinct string // Force the query to only return distinct results. + lockInfo string // Lock for update or in shared lock. + cacheEnabled bool // Enable sql result cache feature, which is mainly for indicating cache duration(especially 0) usage. + cacheOption CacheOption // Cache option for query statement. + pageCacheOption []CacheOption // Cache option for paging query statement. + hookHandler HookHandler // Hook functions for model hook feature. + unscoped bool // Disables soft deleting features when select/delete operations. + safe bool // If true, it clones and returns a new model object whenever operation done; or else it changes the attribute of current model. + onDuplicate any // onDuplicate is used for on Upsert clause. + onDuplicateEx any // onDuplicateEx is used for excluding some columns on Upsert clause. + onConflict any // onConflict is used for conflict keys on Upsert clause. + tableAliasMap map[string]string // Table alias to true table name, usually used in join statements. + softTimeOption SoftTimeOption // SoftTimeOption is the option to customize soft time feature for Model. + shardingConfig ShardingConfig // ShardingConfig for database/table sharding feature. + shardingValue any // Sharding value for sharding feature. } // ModelHandler is a function that handles given Model and returns a new Model that is custom modified. diff --git a/database/gdb/gdb_model_cache.go b/database/gdb/gdb_model_cache.go index dd7dac7fa..3d5fe229a 100644 --- a/database/gdb/gdb_model_cache.go +++ b/database/gdb/gdb_model_cache.go @@ -50,6 +50,18 @@ func (m *Model) Cache(option CacheOption) *Model { return model } +// PageCache sets the cache feature for pagination queries. It allows to configure +// separate cache options for count query and data query in pagination. +// +// Note that, the cache feature is disabled if the model is performing select statement +// on a transaction. +func (m *Model) PageCache(countOption CacheOption, dataOption CacheOption) *Model { + model := m.getModel() + model.pageCacheOption = []CacheOption{countOption, dataOption} + model.cacheEnabled = true + return model +} + // checkAndRemoveSelectCache checks and removes the cache in insert/update/delete statement if // cache feature is enabled. func (m *Model) checkAndRemoveSelectCache(ctx context.Context) { diff --git a/database/gdb/gdb_model_lock.go b/database/gdb/gdb_model_lock.go index a207a126e..3fb4e1f45 100644 --- a/database/gdb/gdb_model_lock.go +++ b/database/gdb/gdb_model_lock.go @@ -6,16 +6,124 @@ package gdb +// Lock clause constants for different databases. +// These constants provide type-safe and IDE-friendly access to various lock syntaxes. +const ( + // Common lock clauses (supported by most databases) + LockForUpdate = "FOR UPDATE" + LockForUpdateSkipLocked = "FOR UPDATE SKIP LOCKED" + + // MySQL lock clauses + LockInShareMode = "LOCK IN SHARE MODE" // MySQL legacy syntax + LockForShare = "FOR SHARE" // MySQL 8.0+ and PostgreSQL + LockForUpdateNowait = "FOR UPDATE NOWAIT" // MySQL 8.0+ and Oracle + + // PostgreSQL specific lock clauses + LockForNoKeyUpdate = "FOR NO KEY UPDATE" + LockForKeyShare = "FOR KEY SHARE" + LockForShareNowait = "FOR SHARE NOWAIT" + LockForShareSkipLocked = "FOR SHARE SKIP LOCKED" + LockForNoKeyUpdateNowait = "FOR NO KEY UPDATE NOWAIT" + LockForNoKeyUpdateSkipLocked = "FOR NO KEY UPDATE SKIP LOCKED" + LockForKeyShareNowait = "FOR KEY SHARE NOWAIT" + LockForKeyShareSkipLocked = "FOR KEY SHARE SKIP LOCKED" + + // Oracle specific lock clauses + LockForUpdateWait5 = "FOR UPDATE WAIT 5" + LockForUpdateWait10 = "FOR UPDATE WAIT 10" + LockForUpdateWait30 = "FOR UPDATE WAIT 30" + + // SQL Server lock hints (use with WITH clause) + LockWithUpdLock = "WITH (UPDLOCK)" + LockWithHoldLock = "WITH (HOLDLOCK)" + LockWithXLock = "WITH (XLOCK)" + LockWithTabLock = "WITH (TABLOCK)" + LockWithNoLock = "WITH (NOLOCK)" + LockWithUpdLockHoldLock = "WITH (UPDLOCK, HOLDLOCK)" +) + +// Lock sets a custom lock clause for the current operation. +// This is a generic method that allows you to specify any lock syntax supported by your database. +// You can use predefined constants or custom strings. +// +// Database-specific lock syntax support: +// +// PostgreSQL (most comprehensive): +// - "FOR UPDATE" - Exclusive lock, blocks all access +// - "FOR NO KEY UPDATE" - Weaker exclusive lock, doesn't block FOR KEY SHARE +// - "FOR SHARE" - Shared lock, allows reads but blocks writes +// - "FOR KEY SHARE" - Weakest lock, only locks key values +// - All above can be combined with: +// - "NOWAIT" - Return immediately if lock cannot be acquired +// - "SKIP LOCKED" - Skip locked rows instead of waiting +// +// MySQL: +// - "FOR UPDATE" - Exclusive lock (all versions) +// - "LOCK IN SHARE MODE" - Shared lock (legacy syntax) +// - "FOR SHARE" - Shared lock (MySQL 8.0+) +// - "FOR UPDATE NOWAIT" - MySQL 8.0+ only +// - "FOR UPDATE SKIP LOCKED" - MySQL 8.0+ only +// +// Oracle: +// - "FOR UPDATE" - Exclusive lock +// - "FOR UPDATE NOWAIT" - Exclusive lock, no wait +// - "FOR UPDATE SKIP LOCKED" - Exclusive lock, skip locked rows +// - "FOR UPDATE WAIT n" - Exclusive lock, wait n seconds +// - "FOR UPDATE OF column_list" - Lock specific columns +// +// SQL Server (uses WITH hints): +// - "WITH (UPDLOCK)" - Update lock +// - "WITH (HOLDLOCK)" - Hold lock until transaction end +// - "WITH (XLOCK)" - Exclusive lock +// - "WITH (TABLOCK)" - Table lock +// - "WITH (NOLOCK)" - No lock (dirty read) +// - "WITH (UPDLOCK, HOLDLOCK)" - Combined update and hold lock +// +// SQLite: +// - Limited locking support, database-level locks only +// - No row-level lock syntax supported +// +// Usage examples: +// +// db.Model("users").Lock("FOR UPDATE NOWAIT").Where("id", 1).One() +// db.Model("users").Lock("FOR SHARE SKIP LOCKED").Where("status", "active").All() +// db.Model("users").Lock("WITH (UPDLOCK)").Where("id", 1).One() // SQL Server +// db.Model("users").Lock("FOR UPDATE OF name, email").Where("id", 1).One() // Oracle +// db.Model("users").Lock("FOR UPDATE WAIT 15").Where("id", 1).One() // Oracle custom wait +// +// Or use predefined constants for better IDE support: +// +// db.Model("users").Lock(gdb.LockForUpdateNowait).Where("id", 1).One() +// db.Model("users").Lock(gdb.LockForShareSkipLocked).Where("status", "active").All() +func (m *Model) Lock(lockClause string) *Model { + model := m.getModel() + model.lockInfo = lockClause + return model +} + // LockUpdate sets the lock for update for current operation. +// This is equivalent to Lock("FOR UPDATE"). func (m *Model) LockUpdate() *Model { model := m.getModel() - model.lockInfo = "FOR UPDATE" + model.lockInfo = LockForUpdate + return model +} + +// LockUpdateSkipLocked sets the lock for update with skip locked behavior for current operation. +// It skips the locked rows. +// This is equivalent to Lock("FOR UPDATE SKIP LOCKED"). +// Note: Supported by PostgreSQL, Oracle, and MySQL 8.0+. +func (m *Model) LockUpdateSkipLocked() *Model { + model := m.getModel() + model.lockInfo = LockForUpdateSkipLocked return model } // LockShared sets the lock in share mode for current operation. +// This is equivalent to Lock("LOCK IN SHARE MODE") for MySQL or Lock("FOR SHARE") for PostgreSQL. +// Note: For maximum compatibility, this uses MySQL's legacy syntax. func (m *Model) LockShared() *Model { model := m.getModel() - model.lockInfo = "LOCK IN SHARE MODE" + model.lockInfo = LockInShareMode return model } diff --git a/database/gdb/gdb_model_select.go b/database/gdb/gdb_model_select.go index 161027899..7d362ac1c 100644 --- a/database/gdb/gdb_model_select.go +++ b/database/gdb/gdb_model_select.go @@ -56,6 +56,9 @@ func (m *Model) AllAndCount(useFieldForCount bool) (result Result, totalCount in if !useFieldForCount { countModel.fields = []any{Raw("1")} } + if len(m.pageCacheOption) > 0 { + countModel = countModel.Cache(m.pageCacheOption[0]) + } // Get the total count of records totalCount, err = countModel.Count() @@ -68,8 +71,13 @@ func (m *Model) AllAndCount(useFieldForCount bool) (result Result, totalCount in return } + resultModel := m.Clone() + if len(m.pageCacheOption) > 1 { + resultModel = resultModel.Cache(m.pageCacheOption[1]) + } + // Retrieve all records - result, err = m.doGetAll(m.GetCtx(), SelectTypeDefault, false) + result, err = resultModel.doGetAll(m.GetCtx(), SelectTypeDefault, false) return } @@ -337,7 +345,9 @@ func (m *Model) ScanAndCount(pointer any, totalCount *int, useFieldForCount bool if !useFieldForCount { countModel.fields = []any{Raw("1")} } - + if len(m.pageCacheOption) > 0 { + countModel = countModel.Cache(m.pageCacheOption[0]) + } // Get the total count of records *totalCount, err = countModel.Count() if err != nil { @@ -348,7 +358,11 @@ func (m *Model) ScanAndCount(pointer any, totalCount *int, useFieldForCount bool if *totalCount == 0 { return } - err = m.Scan(pointer) + scanModel := m.Clone() + if len(m.pageCacheOption) > 1 { + scanModel = scanModel.Cache(m.pageCacheOption[1]) + } + err = scanModel.Scan(pointer) return } @@ -710,8 +724,12 @@ func (m *Model) getFormattedSqlAndArgs( } // Raw SQL Model. if m.rawSql != "" { - sqlWithHolder = fmt.Sprintf("SELECT %s FROM (%s) AS T", queryFields, m.rawSql) - return sqlWithHolder, nil + conditionWhere, conditionExtra, conditionArgs := m.formatCondition(ctx, false, true) + sqlWithHolder = fmt.Sprintf( + "SELECT %s FROM (%s%s) AS T", + queryFields, m.rawSql, conditionWhere+conditionExtra, + ) + return sqlWithHolder, conditionArgs } conditionWhere, conditionExtra, conditionArgs := m.formatCondition(ctx, false, true) sqlWithHolder = fmt.Sprintf("SELECT %s FROM %s%s", queryFields, m.tables, conditionWhere+conditionExtra) diff --git a/database/gdb/gdb_model_utility.go b/database/gdb/gdb_model_utility.go index 4da7dc69e..1ced41c39 100644 --- a/database/gdb/gdb_model_utility.go +++ b/database/gdb/gdb_model_utility.go @@ -68,6 +68,11 @@ func (m *Model) mappingAndFilterToTableFields(table string, fields []any, filter if fieldsTable != "" { hasTable, _ := m.db.GetCore().HasTable(fieldsTable) if !hasTable { + if fieldsTable != m.tablesInit { + // Table/alias unknown (e.g., FieldsPrefix called before LeftJoin), skip filtering. + return fields + } + // HasTable cache miss for main table, fallback to use main table for field mapping. fieldsTable = m.tablesInit } } diff --git a/database/gdb/gdb_panic_recovery_test.go b/database/gdb/gdb_panic_recovery_test.go new file mode 100644 index 000000000..92590290f --- /dev/null +++ b/database/gdb/gdb_panic_recovery_test.go @@ -0,0 +1,159 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package gdb + +import ( + "context" + "database/sql" + "strings" + "testing" + + "github.com/gogf/gf/v2/errors/gcode" + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/test/gtest" +) + +// mockPanicStmt simulates a prepared statement that panics during execution +type mockPanicStmt struct { + panicMessage string +} + +func (m *mockPanicStmt) ExecContext(ctx context.Context, args ...any) (sql.Result, error) { + if m.panicMessage != "" { + panic(m.panicMessage) + } + panic("math/big: buffer too small to fit value") +} + +func (m *mockPanicStmt) QueryContext(ctx context.Context, args ...any) (*sql.Rows, error) { + if m.panicMessage != "" { + panic(m.panicMessage) + } + panic("math/big: buffer too small to fit value") +} + +func (m *mockPanicStmt) QueryRowContext(ctx context.Context, args ...any) *sql.Row { + if m.panicMessage != "" { + panic(m.panicMessage) + } + panic("math/big: buffer too small to fit value") +} + +func (m *mockPanicStmt) Close() error { + return nil +} + +// Test_PanicRecoveryErrorWrapping tests that the panic recovery properly wraps errors +// with correct error codes and messages +func Test_PanicRecoveryErrorWrapping(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + // Test creating an error from a string panic value + defer func() { + if exception := recover(); exception != nil { + var err error + if v, ok := exception.(error); ok && gerror.HasStack(v) { + err = v + } else { + err = gerror.WrapCodef(gcode.CodeDbOperationError, gerror.NewCodef(gcode.CodeInternalPanic, "%+v", exception), "test SQL") + } + + t.AssertNE(err, nil) + t.Assert(strings.Contains(err.Error(), "buffer too small"), true) + t.Assert(strings.Contains(err.Error(), "test SQL"), true) + } + }() + + // Simulate the panic that would occur in database operations + panic("math/big: buffer too small to fit value") + }) + + gtest.C(t, func(t *gtest.T) { + // Test creating an error from an error panic value with stack + defer func() { + if exception := recover(); exception != nil { + var err error + if v, ok := exception.(error); ok && gerror.HasStack(v) { + err = v + } else { + err = gerror.WrapCodef(gcode.CodeDbOperationError, gerror.NewCodef(gcode.CodeInternalPanic, "%+v", exception), "test SQL") + } + + t.AssertNE(err, nil) + // Since gerror has stack, it should preserve the original error + t.Assert(strings.Contains(err.Error(), "custom database error"), true) + } + }() + + // Simulate a panic with a custom error that has stack + customErr := gerror.New("custom database error") + panic(customErr) + }) +} + +// Test_DoCommit_StmtPanicRecovery simulates the scenario from the issue where +// statement execution causes a panic during DoCommit operations +func Test_DoCommit_StmtPanicRecovery(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + // We'll test the panic recovery by triggering it in the defer function + // Since we can't easily mock sql.Stmt, we'll test the panic recovery mechanism directly + + testPanicRecovery := func(panicValue any, sqlText string) (err error) { + defer func() { + if exception := recover(); exception != nil { + if err == nil { + if v, ok := exception.(error); ok && gerror.HasStack(v) { + err = v + } else { + err = gerror.WrapCodef(gcode.CodeDbOperationError, gerror.NewCodef(gcode.CodeInternalPanic, "%+v", exception), FormatSqlWithArgs(sqlText, []any{123})) + } + } + } + }() + + // Simulate the panic that would occur in database operations + panic(panicValue) + } + + // Test different panic scenarios + testCases := []struct { + name string + panicValue any + sqlText string + }{ + { + name: "String panic from math/big", + panicValue: "math/big: buffer too small to fit value", + sqlText: "INSERT INTO test VALUES (?)", + }, + { + name: "Custom error panic", + panicValue: gerror.New("clickhouse driver panic"), + sqlText: "SELECT * FROM test WHERE id = ?", + }, + } + + for _, tc := range testCases { + t.Log("Testing:", tc.name) + + // Test the panic recovery mechanism + err := testPanicRecovery(tc.panicValue, tc.sqlText) + + // After our fix, these should return errors instead of panicking + t.AssertNE(err, nil) + + // Verify the error contains information about the panic + errorMsg := err.Error() + + if tc.name == "String panic from math/big" { + t.Assert(strings.Contains(errorMsg, "buffer too small"), true) + t.Assert(strings.Contains(errorMsg, "INSERT INTO test VALUES"), true) + } else { + t.Assert(strings.Contains(errorMsg, "clickhouse driver panic"), true) + } + } + }) +} diff --git a/database/gdb/gdb_z_core_config_external_test.go b/database/gdb/gdb_z_core_config_external_test.go index bc3749512..9df60c65b 100644 --- a/database/gdb/gdb_z_core_config_external_test.go +++ b/database/gdb/gdb_z_core_config_external_test.go @@ -8,6 +8,7 @@ package gdb_test import ( "testing" + "time" "github.com/gogf/gf/v2/database/gdb" "github.com/gogf/gf/v2/test/gtest" @@ -1189,3 +1190,40 @@ func Test_IsConfigured(t *testing.T) { t.Assert(result, true) }) } + +func Test_ConfigNode_ConnectionPoolSettings(t *testing.T) { + // Test connection pool configuration fields + gtest.C(t, func(t *gtest.T) { + // Save original config and restore after test + originalConfig := gdb.GetAllConfig() + defer func() { + gdb.SetConfig(originalConfig) + }() + + // Reset config + gdb.SetConfig(make(gdb.Config)) + + testNode := gdb.ConfigNode{ + Host: "127.0.0.1", + Port: "3306", + User: "root", + Pass: "123456", + Name: "test_db", + Type: "mysql", + MaxIdleConnCount: 10, + MaxOpenConnCount: 100, + MaxConnLifeTime: 30 * time.Second, + MaxIdleConnTime: 10 * time.Second, + } + + err := gdb.AddConfigNode("pool_test", testNode) + t.AssertNil(err) + + result := gdb.GetAllConfig() + t.Assert(len(result), 1) + t.Assert(result["pool_test"][0].MaxIdleConnCount, 10) + t.Assert(result["pool_test"][0].MaxOpenConnCount, 100) + t.Assert(result["pool_test"][0].MaxConnLifeTime, 30*time.Second) + t.Assert(result["pool_test"][0].MaxIdleConnTime, 10*time.Second) + }) +} diff --git a/database/gdb/gdb_z_core_config_test.go b/database/gdb/gdb_z_core_config_test.go index d0e876192..0deb8ab58 100644 --- a/database/gdb/gdb_z_core_config_test.go +++ b/database/gdb/gdb_z_core_config_test.go @@ -142,6 +142,11 @@ func Test_Core_SetMaxConnections(t *testing.T) { testDuration := time.Hour core.SetMaxConnLifeTime(testDuration) t.Assert(core.dynamicConfig.MaxConnLifeTime, testDuration) + + // Test SetMaxIdleConnTime + idleTimeDuration := 30 * time.Minute + core.SetMaxIdleConnTime(idleTimeDuration) + t.Assert(core.dynamicConfig.MaxIdleConnTime, idleTimeDuration) }) } diff --git a/database/gredis/gredis_config.go b/database/gredis/gredis_config.go index 69d410a8f..6b1124f25 100644 --- a/database/gredis/gredis_config.go +++ b/database/gredis/gredis_config.go @@ -50,8 +50,10 @@ const ( ) var ( + // configChecker checks whether the *Config is nil. + configChecker = func(v *Config) bool { return v == nil } // Configuration groups. - localConfigMap = gmap.NewStrAnyMap(true) + localConfigMap = gmap.NewKVMapWithChecker[string, *Config](configChecker, true) ) // SetConfig sets the global configuration for specified group. @@ -119,7 +121,7 @@ func GetConfig(name ...string) (config *Config, ok bool) { group = name[0] } if v := localConfigMap.Get(group); v != nil { - return v.(*Config), true + return v, true } return &Config{}, false } diff --git a/database/gredis/gredis_instance.go b/database/gredis/gredis_instance.go index 68a2f67e2..1f44368c3 100644 --- a/database/gredis/gredis_instance.go +++ b/database/gredis/gredis_instance.go @@ -14,8 +14,9 @@ import ( ) var ( - // localInstances for instance management of redis client. - localInstances = gmap.NewStrAnyMap(true) + // checker is the checker function for instances map. + checker = func(v *Redis) bool { return v == nil } + localInstances = gmap.NewKVMapWithChecker[string, *Redis](checker, true) ) // Instance returns an instance of redis client with specified group. @@ -26,7 +27,7 @@ func Instance(name ...string) *Redis { if len(name) > 0 && name[0] != "" { group = name[0] } - v := localInstances.GetOrSetFuncLock(group, func() any { + return localInstances.GetOrSetFuncLock(group, func() *Redis { if config, ok := GetConfig(group); ok { r, err := New(config) if err != nil { @@ -37,8 +38,4 @@ func Instance(name ...string) *Redis { } return nil }) - if v != nil { - return v.(*Redis) - } - return nil } diff --git a/encoding/gjson/gjson.go b/encoding/gjson/gjson.go index a939fb6eb..b2f915606 100644 --- a/encoding/gjson/gjson.go +++ b/encoding/gjson/gjson.go @@ -21,7 +21,7 @@ import ( "github.com/gogf/gf/v2/util/gconv" ) -type ContentType string +type ContentType = string const ( ContentTypeJSON ContentType = `json` @@ -35,23 +35,40 @@ const ( ) const ( - defaultSplitChar = '.' // Separator char for hierarchical data access. + // Separator char for hierarchical data access. + defaultSplitChar = '.' ) // Json is the customized JSON struct. type Json struct { mu rwmutex.RWMutex - p *any // Pointer for hierarchical data access, it's the root of data in default. - c byte // Char separator('.' in default). - vc bool // Violence Check(false in default), which is used to access data when the hierarchical data key contains separator char. + + // Pointer for hierarchical data access, it's the root of data in default. + p *any + + // Char separator('.' in default). + c byte + + // Violence Check(false in default), + // which is used to access data when the hierarchical data key contains separator char. + vc bool } // Options for Json object creating/loading. type Options struct { - Safe bool // Mark this object is for in concurrent-safe usage. This is especially for Json object creating. - Tags string // Custom priority tags for decoding, eg: "json,yaml,MyTag". This is especially for struct parsing into Json object. - Type ContentType // Type specifies the data content type, eg: json, xml, yaml, toml, ini. - StrNumber bool // StrNumber causes the Decoder to unmarshal a number into an any as a string instead of as a float64. + // Mark this object is for in concurrent-safe usage. This is especially for Json object creating. + Safe bool + + // Custom priority tags for decoding, eg: "json,yaml,MyTag". + // This is specially for struct parsing into Json object. + Tags string + + // Type specifies the data content type, eg: json, xml, yaml, toml, ini. + Type ContentType + + // StrNumber causes the Decoder to unmarshal a number into an any as a string instead of as a float64. + // This is specially for json content parsing into Json object. + StrNumber bool } // iInterfaces is used for type assert api for Interfaces(). diff --git a/encoding/gjson/gjson_api_new_load_content.go b/encoding/gjson/gjson_api_new_load_content.go index fa102e910..5ae5c6f9e 100644 --- a/encoding/gjson/gjson_api_new_load_content.go +++ b/encoding/gjson/gjson_api_new_load_content.go @@ -161,56 +161,86 @@ func loadContentWithOptions(data []byte, options Options) (*Json, error) { if len(data) == 0 { return NewWithOptions(nil, options), nil } - if options.Type == "" { - options.Type, err = checkDataType(data) + var ( + checkType ContentType + decodedData any + ) + if options.Type != "" { + checkType = gstr.TrimLeft(options.Type, ".") + } else { + checkType, err = checkDataType(data) if err != nil { return nil, err } } - options.Type = ContentType(gstr.TrimLeft( - string(options.Type), "."), - ) - switch options.Type { + switch checkType { case ContentTypeJSON, ContentTypeJs: + decoder := json.NewDecoder(bytes.NewReader(data)) + if options.StrNumber { + decoder.UseNumber() + } + if err = decoder.Decode(&result); err != nil { + return nil, err + } + switch result.(type) { + case string, []byte: + return nil, gerror.Newf(`json decoding failed for content: %s`, data) + } + return NewWithOptions(result, options), nil case ContentTypeXML: - data, err = gxml.ToJson(data) + decodedData, err = gxml.Decode(data) + if err != nil { + return nil, err + } + return NewWithOptions(decodedData, options), nil case ContentTypeYaml, ContentTypeYml: - data, err = gyaml.ToJson(data) + decodedData, err = gyaml.Decode(data) + if err != nil { + return nil, err + } + return NewWithOptions(decodedData, options), nil case ContentTypeToml: - data, err = gtoml.ToJson(data) + decodedData, err = gtoml.Decode(data) + if err != nil { + return nil, err + } + return NewWithOptions(decodedData, options), nil case ContentTypeIni: - data, err = gini.ToJson(data) + decodedData, err = gini.Decode(data) + if err != nil { + return nil, err + } + return NewWithOptions(decodedData, options), nil case ContentTypeProperties: - data, err = gproperties.ToJson(data) + decodedData, err = gproperties.Decode(data) + if err != nil { + return nil, err + } + return NewWithOptions(decodedData, options), nil default: - err = gerror.NewCodef( - gcode.CodeInvalidParameter, - `unsupported type "%s" for loading`, - options.Type, - ) - } - if err != nil { - return nil, err - } - decoder := json.NewDecoder(bytes.NewReader(data)) - if options.StrNumber { - decoder.UseNumber() } - if err = decoder.Decode(&result); err != nil { - return nil, err + // ignore some duplicated types, like js and yml, + // which are not necessary shown in error message. + allSupportedTypes := []string{ + ContentTypeJSON, + ContentTypeXML, + ContentTypeYaml, + ContentTypeToml, + ContentTypeIni, + ContentTypeProperties, } - switch result.(type) { - case string, []byte: - return nil, gerror.Newf(`json decoding failed for content: %s`, data) - } - return NewWithOptions(result, options), nil + return nil, gerror.NewCodef( + gcode.CodeInvalidParameter, + `unsupported type "%s" for loading, all supported types: %s`, + options.Type, gstr.Join(allSupportedTypes, ", "), + ) } // checkDataType automatically checks and returns the data type for `content`. @@ -247,33 +277,104 @@ func checkDataType(data []byte) (ContentType, error) { } } +// isXMLContent checks whether given content is XML format. +// XML format is easy to be identified using regular expression. func isXMLContent(data []byte) bool { return gregex.IsMatch(`^\s*<.+>[\S\s]+<.+>\s*$`, data) } +// isYamlContent checks whether given content is YAML format. func isYamlContent(data []byte) bool { - return !gregex.IsMatch(`[\n\r]*[\s\t\w\-\."]+\s*=\s*"""[\s\S]+"""`, data) && - !gregex.IsMatch(`[\n\r]*[\s\t\w\-\."]+\s*=\s*'''[\s\S]+'''`, data) && - ((gregex.IsMatch(`^[\n\r]*[\w\-\s\t]+\s*:\s*".+"`, data) || - gregex.IsMatch(`^[\n\r]*[\w\-\s\t]+\s*:\s*\w+`, data)) || - (gregex.IsMatch(`[\n\r]+[\w\-\s\t]+\s*:\s*".+"`, data) || - gregex.IsMatch(`[\n\r]+[\w\-\s\t]+\s*:\s*\w+`, data))) + // x = y + // "x.x" = "y" + tomlFormat1 := gregex.IsMatch(`[\n\r]*[\s\t\w\-\."]+\s*=\s*"""[\s\S]+"""`, data) + if tomlFormat1 { + return false + } + // "x.x" = ''' + // y + // ''' + tomlFormat2 := gregex.IsMatch(`[\n\r]*[\s\t\w\-\."]+\s*=\s*'''[\s\S]+'''`, data) + if tomlFormat2 { + return false + } + + // content starts with: + // x : "y" + yamlFormat1 := gregex.IsMatch(`^[\n\r]*[\w\-\s\t]+\s*:\s+".+"`, data) + + // content starts with: + // x : y + yamlFormat2 := gregex.IsMatch(`^[\n\r]*[\w\-\s\t]+\s*:\s+\w+`, data) + + // line starts with: + // x : "y" + yamlFormat3 := gregex.IsMatch(`[\n\r]+[\w\-\s\t]+\s*:\s+".+"`, data) + + // line starts with: + // x : y + yamlFormat4 := gregex.IsMatch(`[\n\r]+[\w\-\s\t]+\s*:\s+\w+`, data) + + // content starts with: + // "x" : "y" + yamlFormat5 := gregex.IsMatch(`^[\n\r]*".+":\s+".+"`, data) + + // line starts with: + // "x" : y + yamlFormat6 := gregex.IsMatch(`[\n\r]+".+":\s+\w+`, data) + + return yamlFormat1 || yamlFormat2 || yamlFormat3 || yamlFormat4 || yamlFormat5 || yamlFormat6 } +// isTomlContent checks whether given content is TOML format. func isTomlContent(data []byte) bool { - return !gregex.IsMatch(`^[\s\t\n\r]*;.+`, data) && - !gregex.IsMatch(`[\s\t\n\r]+;.+`, data) && - !gregex.IsMatch(`[\n\r]+[\s\t\w\-]+\.[\s\t\w\-]+\s*=\s*.+`, data) && - (gregex.IsMatch(`[\n\r]*[\s\t\w\-\."]+\s*=\s*".+"`, data) || - gregex.IsMatch(`[\n\r]*[\s\t\w\-\."]+\s*=\s*\w+`, data)) + // content starts with: + // ; comment line + contentStartsWithSemicolonComment := gregex.IsMatch(`^[\s\t\n\r]*;.+`, data) + if contentStartsWithSemicolonComment { + return false + } + // line starts with: + // ; comment line + lineStartsWithSemicolonComment := gregex.IsMatch(`[\s\t\n\r]+;.+`, data) + if lineStartsWithSemicolonComment { + return false + } + + // line starts with, this should not be toml format: + // key.with.dot = value + keyWithDot := gregex.IsMatch(`[\n\r]+[\s\t\w\-]+\.[\s\t\w\-]+\s*=\s*.+`, data) + if keyWithDot { + return false + } + + // line starts with: + // key = value + // key = "value" + // "key" = "value" + // "key" = value + tomlFormat1 := gregex.IsMatch(`[\n\r]*[\s\t\w\-\."]+\s*=\s*".+"`, data) + tomlFormat2 := gregex.IsMatch(`[\n\r]*[\s\t\w\-\."]+\s*=\s*\w+`, data) + return tomlFormat1 || tomlFormat2 } +// isIniContent checks whether given content is INI format. func isIniContent(data []byte) bool { - return gregex.IsMatch(`\[[\w\.]+\]`, data) && - (gregex.IsMatch(`[\n\r]*[\s\t\w\-\."]+\s*=\s*".+"`, data) || - gregex.IsMatch(`[\n\r]*[\s\t\w\-\."]+\s*=\s*\w+`, data)) + // no section like: [section], but ini format must have sections. + hasBrackets := gregex.IsMatch(`\[[\w\.]+\]`, data) + if !hasBrackets { + return false + } + iniFormat1 := gregex.IsMatch(`[\n\r]*[\s\t\w\-\."]+\s*=\s*".+"`, data) + iniFormat2 := gregex.IsMatch(`[\n\r]*[\s\t\w\-\."]+\s*=\s*\w+`, data) + return iniFormat1 || iniFormat2 } +// isPropertyContent checks whether given content is Properties format. func isPropertyContent(data []byte) bool { - return gregex.IsMatch(`[\n\r]*[\s\t\w\-\."]+\s*=\s*\w+`, data) + // line starts with: + // key = value + // "key" = value + propertyFormat := gregex.IsMatch(`[\n\r]*[\s\t\w\-\."]+\s*=\s*\w+`, data) + return propertyFormat } diff --git a/encoding/gjson/gjson_api_new_load_path.go b/encoding/gjson/gjson_api_new_load_path.go index 45aaa97cb..ccb7a49ea 100644 --- a/encoding/gjson/gjson_api_new_load_path.go +++ b/encoding/gjson/gjson_api_new_load_path.go @@ -29,7 +29,7 @@ func LoadPath(path string, options Options) (*Json, error) { path = p } if options.Type == "" { - options.Type = ContentType(gfile.Ext(path)) + options.Type = gfile.Ext(path) } - return loadContentWithOptions(gfile.GetBytesWithCache(path), options) + return loadContentWithOptions(gfile.GetBytes(path), options) } diff --git a/encoding/gjson/gjson_z_unit_feature_load_test.go b/encoding/gjson/gjson_z_unit_feature_load_test.go index 4a1f5e098..116b3d8da 100644 --- a/encoding/gjson/gjson_z_unit_feature_load_test.go +++ b/encoding/gjson/gjson_z_unit_feature_load_test.go @@ -418,3 +418,13 @@ DBINFO.password=password t.AssertNE(err, nil) }) } + +func Test_Load_YAML_For_I18n(t *testing.T) { + var data = []byte(gtest.DataContent("yaml", "i18n-issue.yaml")) + gtest.C(t, func(t *gtest.T) { + j, err := gjson.LoadContent(data) + t.AssertNil(err) + j.SetViolenceCheck(true) + t.Assert(j.Get("resourceUsage.workflow").String(), "workflow") + }) +} diff --git a/encoding/gjson/testdata/yaml/i18n-issue.yaml b/encoding/gjson/testdata/yaml/i18n-issue.yaml new file mode 100644 index 000000000..8a2ecd704 --- /dev/null +++ b/encoding/gjson/testdata/yaml/i18n-issue.yaml @@ -0,0 +1,16 @@ +"environment status is Creating/Updating, please wait for sync to complete": "环境当前状为创建中/更新中,请等待同步完成" +"There are still queues in the current environment, please ensure there are no queues before deletion": "当前环境还存在队列,确保环境没有队列再删除" +"the current repository has associated environments in use, please ensure no environment associations before deleting the repository": "当前仓库有关联环境正在使用,请确保没有环境关联再删除该仓库" +"There are environments using this cluster, please ensure all environments have been deleted before deleting the cluster": "当前集群存在环境正在使用,请确保所有环境已经删除再删除该集群" + +"shareStrategy.Init": "未拆卡" +"shareStrategy.Pending": "切分中" +"shareStrategy.Success": "拆卡成功" +"shareStrategy.Canceling": "拆卡取消中" +"shareStrategy.unknown": "未知状态" +"resourceUsage.none": "无" +"resourceUsage.inference": "推理" +"resourceUsage.training": "训练" +"resourceUsage.workflow": "workflow" +"resourceUsage.hybrid": "混合" +"resourceUsage.unknown": "unknown" \ No newline at end of file diff --git a/errors/gerror/gerror.go b/errors/gerror/gerror.go index e252d872a..514efe4e2 100644 --- a/errors/gerror/gerror.go +++ b/errors/gerror/gerror.go @@ -17,40 +17,48 @@ import ( // IEqual is the interface for Equal feature. type IEqual interface { - Error() string + error Equal(target error) bool } // ICode is the interface for Code feature. type ICode interface { - Error() string + error Code() gcode.Code } // IStack is the interface for Stack feature. type IStack interface { - Error() string + error Stack() string } // ICause is the interface for Cause feature. type ICause interface { - Error() string + error Cause() error } // ICurrent is the interface for Current feature. type ICurrent interface { - Error() string + error Current() error } // IUnwrap is the interface for Unwrap feature. type IUnwrap interface { - Error() string + error Unwrap() error } +// ITextArgs is the interface for Text and Args features. +// This interface is mainly used for i18n features, that needs text and args separately. +type ITextArgs interface { + error + Text() string + Args() []any +} + const ( // commaSeparatorSpace is the comma separator with space. commaSeparatorSpace = ", " diff --git a/errors/gerror/gerror_api.go b/errors/gerror/gerror_api.go index 3b2b3915f..e2c6a836e 100644 --- a/errors/gerror/gerror_api.go +++ b/errors/gerror/gerror_api.go @@ -7,8 +7,6 @@ package gerror import ( - "fmt" - "github.com/gogf/gf/v2/errors/gcode" ) @@ -25,7 +23,8 @@ func New(text string) error { func Newf(format string, args ...any) error { return &Error{ stack: callers(), - text: fmt.Sprintf(format, args...), + text: format, + args: args, code: gcode.CodeNil, } } @@ -45,7 +44,8 @@ func NewSkip(skip int, text string) error { func NewSkipf(skip int, format string, args ...any) error { return &Error{ stack: callers(skip), - text: fmt.Sprintf(format, args...), + text: format, + args: args, code: gcode.CodeNil, } } @@ -74,7 +74,8 @@ func Wrapf(err error, format string, args ...any) error { return &Error{ error: err, stack: callers(), - text: fmt.Sprintf(format, args...), + text: format, + args: args, code: Code(err), } } @@ -104,7 +105,8 @@ func WrapSkipf(skip int, err error, format string, args ...any) error { return &Error{ error: err, stack: callers(skip), - text: fmt.Sprintf(format, args...), + text: format, + args: args, code: Code(err), } } diff --git a/errors/gerror/gerror_api_code.go b/errors/gerror/gerror_api_code.go index c13305ca8..465baa8ea 100644 --- a/errors/gerror/gerror_api_code.go +++ b/errors/gerror/gerror_api_code.go @@ -7,7 +7,6 @@ package gerror import ( - "fmt" "strings" "github.com/gogf/gf/v2/errors/gcode" @@ -26,7 +25,8 @@ func NewCode(code gcode.Code, text ...string) error { func NewCodef(code gcode.Code, format string, args ...any) error { return &Error{ stack: callers(), - text: fmt.Sprintf(format, args...), + text: format, + args: args, code: code, } } @@ -46,7 +46,8 @@ func NewCodeSkip(code gcode.Code, skip int, text ...string) error { func NewCodeSkipf(code gcode.Code, skip int, format string, args ...any) error { return &Error{ stack: callers(skip), - text: fmt.Sprintf(format, args...), + text: format, + args: args, code: code, } } @@ -74,7 +75,8 @@ func WrapCodef(code gcode.Code, err error, format string, args ...any) error { return &Error{ error: err, stack: callers(), - text: fmt.Sprintf(format, args...), + text: format, + args: args, code: code, } } @@ -104,7 +106,8 @@ func WrapCodeSkipf(code gcode.Code, skip int, err error, format string, args ... return &Error{ error: err, stack: callers(skip), - text: fmt.Sprintf(format, args...), + text: format, + args: args, code: code, } } diff --git a/errors/gerror/gerror_api_option.go b/errors/gerror/gerror_api_option.go index 2da7de8b6..40b7de308 100644 --- a/errors/gerror/gerror_api_option.go +++ b/errors/gerror/gerror_api_option.go @@ -13,6 +13,7 @@ type Option struct { Error error // Wrapped error if any. Stack bool // Whether recording stack information into error. Text string // Error text, which is created by New* functions. + Args []any // Error arguments for formatted error text. Code gcode.Code // Error code if necessary. } @@ -22,6 +23,7 @@ func NewWithOption(option Option) error { err := &Error{ error: option.Error, text: option.Text, + args: option.Args, code: option.Code, } if option.Stack { diff --git a/errors/gerror/gerror_error.go b/errors/gerror/gerror_error.go index 18a181184..79a226f59 100644 --- a/errors/gerror/gerror_error.go +++ b/errors/gerror/gerror_error.go @@ -20,6 +20,7 @@ type Error struct { error error // Wrapped error. stack stack // Stack array, which records the stack information when this error is created or wrapped. text string // Custom Error text when Error is created, might be empty when its code is not nil. + args []any // Custom arguments for formatting the error text. code gcode.Code // Error code if necessary. } @@ -42,7 +43,7 @@ func (err *Error) Error() string { if err == nil { return "" } - errStr := err.text + errStr := err.TextWithArgs() if errStr == "" && err.code != nil { errStr = err.code.Message() } @@ -76,7 +77,7 @@ func (err *Error) Cause() error { // return loop // // To be compatible with Case of https://github.com/pkg/errors. - return errors.New(loop.text) + return errors.New(loop.TextWithArgs()) } } return nil @@ -92,6 +93,7 @@ func (err *Error) Current() error { error: nil, stack: err.stack, text: err.text, + args: err.args, code: err.code, } } @@ -118,8 +120,26 @@ func (err *Error) Equal(target error) bool { return false } // Text should be the same. - if err.text != fmt.Sprintf(`%-s`, target) { + if err.TextWithArgs() != fmt.Sprintf(`%-s`, target) { return false } return true } + +// TextWithArgs returns the formatted error text with its arguments. +func (err *Error) TextWithArgs() string { + if len(err.args) > 0 { + return fmt.Sprintf(err.text, err.args...) + } + return err.text +} + +// Text returns the error text of current error. +func (err *Error) Text() string { + return err.text +} + +// Args returns the error arguments of current error. +func (err *Error) Args() []any { + return err.args +} diff --git a/errors/gerror/gerror_error_format.go b/errors/gerror/gerror_error_format.go index 16be393e6..42f3b4296 100644 --- a/errors/gerror/gerror_error_format.go +++ b/errors/gerror/gerror_error_format.go @@ -23,7 +23,7 @@ func (err *Error) Format(s fmt.State, verb rune) { switch { case s.Flag('-'): if err.text != "" { - _, _ = io.WriteString(s, err.text) + _, _ = io.WriteString(s, err.TextWithArgs()) } else { _, _ = io.WriteString(s, err.Error()) } diff --git a/errors/gerror/gerror_error_json.go b/errors/gerror/gerror_error_json.go index ae245cdd7..bf9465fbe 100644 --- a/errors/gerror/gerror_error_json.go +++ b/errors/gerror/gerror_error_json.go @@ -10,8 +10,8 @@ import ( "encoding/json" ) -// MarshalJSON implements the interface MarshalJSON for json.Marshal. -// Note that do not use pointer as its receiver here. -func (err Error) MarshalJSON() ([]byte, error) { +// MarshalJSON implements the interface json.Marshaler for Error. +// It serializes the error using its string representation. +func (err *Error) MarshalJSON() ([]byte, error) { return json.Marshal(err.Error()) } diff --git a/errors/gerror/gerror_z_unit_test.go b/errors/gerror/gerror_z_unit_test.go index eb4e97d4d..e7ff53698 100644 --- a/errors/gerror/gerror_z_unit_test.go +++ b/errors/gerror/gerror_z_unit_test.go @@ -804,3 +804,26 @@ func Test_WrapCodeSkip_MultipleTexts(t *testing.T) { t.Assert(err.Error(), "text1, text2: inner") }) } + +func Test_TextArgs(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + err := gerror.New("text") + textArgs := err.(gerror.ITextArgs) + t.Assert(textArgs.Text(), "text") + t.Assert(textArgs.Args(), nil) + }) + gtest.C(t, func(t *gtest.T) { + err := gerror.Newf("text: %s", "arg1") + textArgs := err.(gerror.ITextArgs) + t.Assert(textArgs.Text(), "text: %s") + t.Assert(textArgs.Args(), []any{"arg1"}) + }) + gtest.C(t, func(t *gtest.T) { + err1 := errors.New("text") + err2 := gerror.Wrapf(err1, "wrap: %s", "arg1") + textArgs := err2.(gerror.ITextArgs) + t.Assert(textArgs.Error(), "wrap: arg1: text") + t.Assert(textArgs.Text(), "wrap: %s") + t.Assert(textArgs.Args(), []any{"arg1"}) + }) +} diff --git a/i18n/gi18n/gi18n_instance.go b/i18n/gi18n/gi18n_instance.go index 8a41c34ad..465888c91 100644 --- a/i18n/gi18n/gi18n_instance.go +++ b/i18n/gi18n/gi18n_instance.go @@ -14,9 +14,11 @@ const ( ) var ( + // checker is used for checking whether the value is nil. + checker = func(v *Manager) bool { return v == nil } // instances is the instances map for management // for multiple i18n instance by name. - instances = gmap.NewStrAnyMap(true) + instances = gmap.NewKVMapWithChecker[string, *Manager](checker, true) ) // Instance returns an instance of Resource. @@ -26,7 +28,7 @@ func Instance(name ...string) *Manager { if len(name) > 0 && name[0] != "" { key = name[0] } - return instances.GetOrSetFuncLock(key, func() any { + return instances.GetOrSetFuncLock(key, func() *Manager { return New() - }).(*Manager) + }) } diff --git a/i18n/gi18n/gi18n_manager.go b/i18n/gi18n/gi18n_manager.go index 6c126d09c..6b9fa4f60 100644 --- a/i18n/gi18n/gi18n_manager.go +++ b/i18n/gi18n/gi18n_manager.go @@ -298,7 +298,7 @@ func (m *Manager) init(ctx context.Context) { if m.data[lang] == nil { m.data[lang] = make(map[string]string) } - if j, err := gjson.LoadContent(gfile.GetBytes(file)); err == nil { + if j, err := gjson.LoadPath(file, gjson.Options{}); err == nil { for k, v := range j.Var().Map() { m.data[lang][k] = gconv.String(v) } diff --git a/i18n/gi18n/gi18n_z_unit_test.go b/i18n/gi18n/gi18n_z_unit_test.go index 2a1d672a4..ef03a622b 100644 --- a/i18n/gi18n/gi18n_z_unit_test.go +++ b/i18n/gi18n/gi18n_z_unit_test.go @@ -259,3 +259,25 @@ func Test_PathInNormal(t *testing.T) { t.Assert(i18n.T(context.Background(), "{#lang}"), "en-US") }) } + +func Test_Issue_Yaml(t *testing.T) { + // Copy i18n files to current directory. + err := gfile.CopyDir( + gtest.DataPath("issue-yaml"), + gfile.Join(gdebug.CallerDirectory(), "manifest/i18n"), + ) + // Remove copied files after testing. + defer gfile.RemoveAll(gfile.Join(gdebug.CallerDirectory(), "manifest")) + + gtest.AssertNil(err) + + var ( + i18n = gi18n.New() + ctx = context.Background() + ) + + gtest.C(t, func(t *gtest.T) { + i18n.SetLanguage("zh") + t.Assert(i18n.T(ctx, "{#resourceUsage.workflow}"), "workflow") + }) +} diff --git a/i18n/gi18n/testdata/issue-yaml/zh.yaml b/i18n/gi18n/testdata/issue-yaml/zh.yaml new file mode 100644 index 000000000..8a2ecd704 --- /dev/null +++ b/i18n/gi18n/testdata/issue-yaml/zh.yaml @@ -0,0 +1,16 @@ +"environment status is Creating/Updating, please wait for sync to complete": "环境当前状为创建中/更新中,请等待同步完成" +"There are still queues in the current environment, please ensure there are no queues before deletion": "当前环境还存在队列,确保环境没有队列再删除" +"the current repository has associated environments in use, please ensure no environment associations before deleting the repository": "当前仓库有关联环境正在使用,请确保没有环境关联再删除该仓库" +"There are environments using this cluster, please ensure all environments have been deleted before deleting the cluster": "当前集群存在环境正在使用,请确保所有环境已经删除再删除该集群" + +"shareStrategy.Init": "未拆卡" +"shareStrategy.Pending": "切分中" +"shareStrategy.Success": "拆卡成功" +"shareStrategy.Canceling": "拆卡取消中" +"shareStrategy.unknown": "未知状态" +"resourceUsage.none": "无" +"resourceUsage.inference": "推理" +"resourceUsage.training": "训练" +"resourceUsage.workflow": "workflow" +"resourceUsage.hybrid": "混合" +"resourceUsage.unknown": "unknown" \ No newline at end of file diff --git a/internal/httputil/httputils.go b/internal/httputil/httputils.go index 41603e042..22cabfa19 100644 --- a/internal/httputil/httputils.go +++ b/internal/httputil/httputils.go @@ -13,7 +13,6 @@ import ( "github.com/gogf/gf/v2/encoding/gurl" "github.com/gogf/gf/v2/internal/empty" - "github.com/gogf/gf/v2/text/gstr" "github.com/gogf/gf/v2/util/gconv" ) @@ -47,15 +46,6 @@ func BuildParams(params any, noUrlEncode ...bool) (encodedParamStr string) { if len(noUrlEncode) == 1 { urlEncode = !noUrlEncode[0] } - // If there's file uploading, it ignores the url encoding. - if urlEncode { - for k, v := range m { - if gstr.Contains(k, fileUploadingKey) || gstr.Contains(gconv.String(v), fileUploadingKey) { - urlEncode = false - break - } - } - } s := "" for k, v := range m { // Ignore nil attributes. @@ -67,8 +57,8 @@ func BuildParams(params any, noUrlEncode ...bool) (encodedParamStr string) { } s = gconv.String(v) if urlEncode { - if strings.HasPrefix(s, fileUploadingKey) && len(s) > len(fileUploadingKey) { - // No url encoding if uploading file. + if strings.HasPrefix(s, fileUploadingKey) { + // No url encoding if value starts with file uploading marker. } else { s = gurl.Encode(s) } diff --git a/internal/httputil/httputils_test.go b/internal/httputil/httputils_test.go index 8833b9534..24a3ba333 100644 --- a/internal/httputil/httputils_test.go +++ b/internal/httputil/httputils_test.go @@ -51,3 +51,132 @@ func TestIssue4023(t *testing.T) { t.Assert(params, "key1=value1") }) } + +// TestBuildParams_SpecialCharacters tests URL encoding of special characters. +func TestBuildParams_SpecialCharacters(t *testing.T) { + // Test special characters are properly URL encoded. + gtest.C(t, func(t *gtest.T) { + data := g.Map{ + "key": "value=with=equals", + } + params := httputil.BuildParams(data) + // = should be encoded as %3D + t.Assert(gstr.Contains(params, "key=value%3Dwith%3Dequals"), true) + }) + + gtest.C(t, func(t *gtest.T) { + data := g.Map{ + "key": "value&with&ersand", + } + params := httputil.BuildParams(data) + // & should be encoded as %26 + t.Assert(gstr.Contains(params, "key=value%26with%26ampersand"), true) + }) + + gtest.C(t, func(t *gtest.T) { + data := g.Map{ + "key": "value with spaces", + } + params := httputil.BuildParams(data) + // space should be encoded as + or %20 + t.Assert(gstr.Contains(params, "key=value") && gstr.Contains(params, "with") && gstr.Contains(params, "spaces"), true) + }) + + gtest.C(t, func(t *gtest.T) { + data := g.Map{ + "key": "value%percent", + } + params := httputil.BuildParams(data) + // % should be encoded as %25 + t.Assert(gstr.Contains(params, "key=value%25percent"), true) + }) +} + +// TestBuildParams_FileUploadMarker tests that @file: prefix is not URL encoded. +func TestBuildParams_FileUploadMarker(t *testing.T) { + // Test @file: with path is not encoded. + gtest.C(t, func(t *gtest.T) { + data := g.Map{ + "file": "@file:/path/to/file.txt", + } + params := httputil.BuildParams(data) + // @file: should NOT be encoded + t.Assert(gstr.Contains(params, "file=@file:/path/to/file.txt"), true) + }) + + // Test @file: without path is not encoded. + gtest.C(t, func(t *gtest.T) { + data := g.Map{ + "name": "@file:", + } + params := httputil.BuildParams(data) + // @file: alone should NOT be encoded + t.Assert(gstr.Contains(params, "name=@file:"), true) + }) + + // Test @file: with path does not affect other fields encoding. + gtest.C(t, func(t *gtest.T) { + data := g.Map{ + "file": "@file:/path/to/file.txt", + "field": "value=1&b=2", + } + params := httputil.BuildParams(data) + // @file: should NOT be encoded + t.Assert(gstr.Contains(params, "@file:/path/to/file.txt"), true) + // Other field's special characters SHOULD be encoded + t.Assert(gstr.Contains(params, "field=value%3D1%26b%3D2"), true) + }) +} + +// TestBuildParams_NoUrlEncode tests the noUrlEncode parameter. +func TestBuildParams_NoUrlEncode(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + data := g.Map{ + "key": "value=1&b=2", + } + // With noUrlEncode = true, special characters should NOT be encoded. + params := httputil.BuildParams(data, true) + t.Assert(gstr.Contains(params, "key=value=1&b=2"), true) + }) + + gtest.C(t, func(t *gtest.T) { + data := g.Map{ + "key": "value=1&b=2", + } + // With noUrlEncode = false (default), special characters SHOULD be encoded. + params := httputil.BuildParams(data, false) + t.Assert(gstr.Contains(params, "key=value%3D1%26b%3D2"), true) + }) +} + +// TestBuildParams_StringInput tests string input is returned as-is. +func TestBuildParams_StringInput(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + data := "key=value&key2=value2" + params := httputil.BuildParams(data) + t.Assert(params, "key=value&key2=value2") + }) + + gtest.C(t, func(t *gtest.T) { + data := []byte("key=value&key2=value2") + params := httputil.BuildParams(data) + t.Assert(params, "key=value&key2=value2") + }) +} + +// TestBuildParams_SliceInput tests slice input. +func TestBuildParams_SliceInput(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + data := []any{g.Map{"a": "1", "b": "2"}} + params := httputil.BuildParams(data) + t.Assert(gstr.Contains(params, "a=1"), true) + t.Assert(gstr.Contains(params, "b=2"), true) + }) + + gtest.C(t, func(t *gtest.T) { + // Empty slice + data := []any{} + params := httputil.BuildParams(data) + t.Assert(params, "") + }) +} diff --git a/net/gclient/gclient_request.go b/net/gclient/gclient_request.go index 8375f30cf..59e22751c 100644 --- a/net/gclient/gclient_request.go +++ b/net/gclient/gclient_request.go @@ -18,6 +18,7 @@ import ( "time" "github.com/gogf/gf/v2/encoding/gjson" + "github.com/gogf/gf/v2/encoding/gurl" "github.com/gogf/gf/v2/errors/gcode" "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/internal/httputil" @@ -248,7 +249,7 @@ func (c *Client) prepareRequest(ctx context.Context, method, url string, data .. isFileUploading = false ) for _, item := range strings.Split(params, "&") { - array := strings.Split(item, "=") + array := strings.SplitN(item, "=", 2) if len(array) < 2 { continue } @@ -287,6 +288,14 @@ func (c *Client) prepareRequest(ctx context.Context, method, url string, data .. fieldName = array[0] fieldValue = array[1] ) + // Decode URL-encoded field name and value. + // If decoding fails, use the original value. + if v, err := gurl.Decode(fieldName); err == nil { + fieldName = v + } + if v, err := gurl.Decode(fieldValue); err == nil { + fieldValue = v + } if err = writer.WriteField(fieldName, fieldValue); err != nil { return nil, gerror.Wrapf( err, `write form field failed with "%s", "%s"`, fieldName, fieldValue, diff --git a/net/gclient/gclient_z_unit_issue_test.go b/net/gclient/gclient_z_unit_issue_test.go index b397f114a..8f7d7fa5d 100644 --- a/net/gclient/gclient_z_unit_issue_test.go +++ b/net/gclient/gclient_z_unit_issue_test.go @@ -80,3 +80,262 @@ func Test_Issue3748(t *testing.T) { t.AssertNil(err) }) } + +// https://github.com/gogf/gf/issues/4156 +func Test_Issue4156(t *testing.T) { + s := g.Server(guid.S()) + s.BindHandler("/upload", func(r *ghttp.Request) { + // Return the fieldName value received + r.Response.Write(r.Get("fieldName")) + }) + s.SetDumpRouterMap(false) + s.Start() + defer s.Shutdown() + + clientHost := fmt.Sprintf("http://127.0.0.1:%d", s.GetListenedPort()) + time.Sleep(100 * time.Millisecond) + + gtest.C(t, func(t *gtest.T) { + client := gclient.New() + client.SetPrefix(clientHost) + // When posting form with file upload, if value contains '=', it should not be truncated. + data := g.Map{ + "file": "@file:" + gtest.DataPath("upload", "file1.txt"), + "fieldName": "aaa=1&b=2", + } + content := client.PostContent(ctx, "/upload", data) + // The complete value should be received, not truncated at '=' + t.Assert(content, "aaa=1&b=2") + }) +} + +// Test_Issue4156_MultipleSpecialChars tests file upload with various special characters in field values. +func Test_Issue4156_MultipleSpecialChars(t *testing.T) { + s := g.Server(guid.S()) + s.BindHandler("/upload", func(r *ghttp.Request) { + r.Response.Write(r.Get("field")) + }) + s.SetDumpRouterMap(false) + s.Start() + defer s.Shutdown() + + clientHost := fmt.Sprintf("http://127.0.0.1:%d", s.GetListenedPort()) + time.Sleep(100 * time.Millisecond) + + // Test with multiple equals signs + gtest.C(t, func(t *gtest.T) { + client := gclient.New() + client.SetPrefix(clientHost) + data := g.Map{ + "file": "@file:" + gtest.DataPath("upload", "file1.txt"), + "field": "a=1=2=3", + } + content := client.PostContent(ctx, "/upload", data) + t.Assert(content, "a=1=2=3") + }) + + // Test with multiple ampersands + gtest.C(t, func(t *gtest.T) { + client := gclient.New() + client.SetPrefix(clientHost) + data := g.Map{ + "file": "@file:" + gtest.DataPath("upload", "file1.txt"), + "field": "a&b&c&d", + } + content := client.PostContent(ctx, "/upload", data) + t.Assert(content, "a&b&c&d") + }) + + // Test with percent sign + gtest.C(t, func(t *gtest.T) { + client := gclient.New() + client.SetPrefix(clientHost) + data := g.Map{ + "file": "@file:" + gtest.DataPath("upload", "file1.txt"), + "field": "100%complete", + } + content := client.PostContent(ctx, "/upload", data) + t.Assert(content, "100%complete") + }) + + // Test with plus sign + gtest.C(t, func(t *gtest.T) { + client := gclient.New() + client.SetPrefix(clientHost) + data := g.Map{ + "file": "@file:" + gtest.DataPath("upload", "file1.txt"), + "field": "1+2+3", + } + content := client.PostContent(ctx, "/upload", data) + t.Assert(content, "1+2+3") + }) + + // Test with spaces + gtest.C(t, func(t *gtest.T) { + client := gclient.New() + client.SetPrefix(clientHost) + data := g.Map{ + "file": "@file:" + gtest.DataPath("upload", "file1.txt"), + "field": "hello world test", + } + content := client.PostContent(ctx, "/upload", data) + t.Assert(content, "hello world test") + }) + + // Test with mixed special characters + gtest.C(t, func(t *gtest.T) { + client := gclient.New() + client.SetPrefix(clientHost) + data := g.Map{ + "file": "@file:" + gtest.DataPath("upload", "file1.txt"), + "field": "key=value&foo=bar%20test+plus", + } + content := client.PostContent(ctx, "/upload", data) + t.Assert(content, "key=value&foo=bar%20test+plus") + }) +} + +// Test_Issue4156_MultipleFields tests file upload with multiple fields containing special characters. +func Test_Issue4156_MultipleFields(t *testing.T) { + s := g.Server(guid.S()) + s.BindHandler("/upload", func(r *ghttp.Request) { + // Return all field values as JSON-like format + r.Response.Writef("field1=%s,field2=%s,field3=%s", + r.Get("field1"), r.Get("field2"), r.Get("field3")) + }) + s.SetDumpRouterMap(false) + s.Start() + defer s.Shutdown() + + clientHost := fmt.Sprintf("http://127.0.0.1:%d", s.GetListenedPort()) + time.Sleep(100 * time.Millisecond) + + gtest.C(t, func(t *gtest.T) { + client := gclient.New() + client.SetPrefix(clientHost) + data := g.Map{ + "file": "@file:" + gtest.DataPath("upload", "file1.txt"), + "field1": "a=1", + "field2": "b&2", + "field3": "c%3", + } + content := client.PostContent(ctx, "/upload", data) + t.Assert(strings.Contains(content, "field1=a=1"), true) + t.Assert(strings.Contains(content, "field2=b&2"), true) + t.Assert(strings.Contains(content, "field3=c%3"), true) + }) +} + +// Test_Issue4156_NoFileUpload tests that normal POST without file upload still works correctly. +func Test_Issue4156_NoFileUpload(t *testing.T) { + s := g.Server(guid.S()) + s.BindHandler("/post", func(r *ghttp.Request) { + r.Response.Write(r.Get("field")) + }) + s.SetDumpRouterMap(false) + s.Start() + defer s.Shutdown() + + clientHost := fmt.Sprintf("http://127.0.0.1:%d", s.GetListenedPort()) + time.Sleep(100 * time.Millisecond) + + // Test normal POST with special characters (no file upload) + gtest.C(t, func(t *gtest.T) { + client := gclient.New() + client.SetPrefix(clientHost) + data := g.Map{ + "field": "a=1&b=2", + } + content := client.PostContent(ctx, "/post", data) + t.Assert(content, "a=1&b=2") + }) + + // Test POST with Content-Type: application/x-www-form-urlencoded + gtest.C(t, func(t *gtest.T) { + client := gclient.New() + client.SetPrefix(clientHost) + client.SetHeader("Content-Type", "application/x-www-form-urlencoded") + data := g.Map{ + "field": "value=with=equals&and&ersand", + } + content := client.PostContent(ctx, "/post", data) + t.Assert(content, "value=with=equals&and&ersand") + }) +} + +// Test_Issue4156_PreEncodedValue tests that pre-encoded values are handled correctly. +func Test_Issue4156_PreEncodedValue(t *testing.T) { + s := g.Server(guid.S()) + s.BindHandler("/upload", func(r *ghttp.Request) { + r.Response.Write(r.Get("field")) + }) + s.SetDumpRouterMap(false) + s.Start() + defer s.Shutdown() + + clientHost := fmt.Sprintf("http://127.0.0.1:%d", s.GetListenedPort()) + time.Sleep(100 * time.Millisecond) + + // Test with already URL-encoded value - should preserve the encoding + gtest.C(t, func(t *gtest.T) { + client := gclient.New() + client.SetPrefix(clientHost) + data := g.Map{ + "file": "@file:" + gtest.DataPath("upload", "file1.txt"), + "field": "value%3Dwith%26encoding", // User wants to send literal %3D + } + content := client.PostContent(ctx, "/upload", data) + // The literal %3D and %26 should be preserved + t.Assert(content, "value%3Dwith%26encoding") + }) +} + +// Test_Issue4156_EmptyAndSpecialValues tests edge cases with empty and special values. +func Test_Issue4156_EmptyAndSpecialValues(t *testing.T) { + s := g.Server(guid.S()) + s.BindHandler("/upload", func(r *ghttp.Request) { + r.Response.Write(r.Get("field")) + }) + s.SetDumpRouterMap(false) + s.Start() + defer s.Shutdown() + + clientHost := fmt.Sprintf("http://127.0.0.1:%d", s.GetListenedPort()) + time.Sleep(100 * time.Millisecond) + + // Test with value starting with = + gtest.C(t, func(t *gtest.T) { + client := gclient.New() + client.SetPrefix(clientHost) + data := g.Map{ + "file": "@file:" + gtest.DataPath("upload", "file1.txt"), + "field": "=startWithEquals", + } + content := client.PostContent(ctx, "/upload", data) + t.Assert(content, "=startWithEquals") + }) + + // Test with value ending with = + gtest.C(t, func(t *gtest.T) { + client := gclient.New() + client.SetPrefix(clientHost) + data := g.Map{ + "file": "@file:" + gtest.DataPath("upload", "file1.txt"), + "field": "endWithEquals=", + } + content := client.PostContent(ctx, "/upload", data) + t.Assert(content, "endWithEquals=") + }) + + // Test with only special characters + gtest.C(t, func(t *gtest.T) { + client := gclient.New() + client.SetPrefix(clientHost) + data := g.Map{ + "file": "@file:" + gtest.DataPath("upload", "file1.txt"), + "field": "=&=&=", + } + content := client.PostContent(ctx, "/upload", data) + t.Assert(content, "=&=&=") + }) +} diff --git a/net/ghttp/ghttp.go b/net/ghttp/ghttp.go index 60ecf1dce..3a58745db 100644 --- a/net/ghttp/ghttp.go +++ b/net/ghttp/ghttp.go @@ -176,9 +176,12 @@ var ( // It is used for quick HTTP method searching using map. methodsMap = make(map[string]struct{}) + // checker is used for checking whether the value is nil. + checker = func(v *Server) bool { return v == nil } + // serverMapping stores more than one server instances for current processes. // The key is the name of the server, and the value is its instance. - serverMapping = gmap.NewStrAnyMap(true) + serverMapping = gmap.NewKVMapWithChecker[string, *Server](checker, true) // serverRunning marks the running server counts. // If there is no successful server running or all servers' shutdown, this value is 0. diff --git a/net/ghttp/ghttp_server.go b/net/ghttp/ghttp_server.go index 57af5c3fb..a486c981c 100644 --- a/net/ghttp/ghttp_server.go +++ b/net/ghttp/ghttp_server.go @@ -96,7 +96,7 @@ func GetServer(name ...any) *Server { if len(name) > 0 && name[0] != "" { serverName = gconv.String(name[0]) } - v := serverMapping.GetOrSetFuncLock(serverName, func() any { + return serverMapping.GetOrSetFuncLock(serverName, func() *Server { s := &Server{ instance: serverName, plugins: make([]Plugin, 0), @@ -118,7 +118,6 @@ func GetServer(name ...any) *Server { s.Use(internalMiddlewareServerTracing) return s }) - return v.(*Server) } // Start starts listening on configured port. @@ -477,10 +476,9 @@ func Wait() { <-allShutdownChan // Remove plugins. - serverMapping.Iterator(func(k string, v any) bool { - s := v.(*Server) - if len(s.plugins) > 0 { - for _, p := range s.plugins { + serverMapping.Iterator(func(k string, v *Server) bool { + if len(v.plugins) > 0 { + for _, p := range v.plugins { intlog.Printf(ctx, `remove plugin: %s`, p.Name()) if err := p.Remove(); err != nil { intlog.Errorf(ctx, `%+v`, err) diff --git a/net/ghttp/ghttp_server_admin_process.go b/net/ghttp/ghttp_server_admin_process.go index f7cf15549..60313218a 100644 --- a/net/ghttp/ghttp_server_admin_process.go +++ b/net/ghttp/ghttp_server_admin_process.go @@ -190,9 +190,9 @@ func forkRestartProcess(ctx context.Context, newExeFilePath ...string) error { // getServerFdMap returns all the servers name to file descriptor mapping as map. func getServerFdMap() map[string]listenerFdMap { sfm := make(map[string]listenerFdMap) - serverMapping.RLockFunc(func(m map[string]any) { + serverMapping.RLockFunc(func(m map[string]*Server) { for k, v := range m { - sfm[k] = v.(*Server).getListenerFdMap() + sfm[k] = v.getListenerFdMap() } }) return sfm @@ -263,11 +263,10 @@ func shutdownWebServersGracefully(ctx context.Context, signal os.Signal) { } else { glog.Printf(ctx, "pid[%d]: server gracefully shutting down by api", gproc.Pid()) } - serverMapping.RLockFunc(func(m map[string]any) { + serverMapping.RLockFunc(func(m map[string]*Server) { for _, v := range m { - server := v.(*Server) - server.doServiceDeregister() - for _, s := range server.servers { + v.doServiceDeregister() + for _, s := range v.servers { s.Shutdown(ctx) } } @@ -276,9 +275,9 @@ func shutdownWebServersGracefully(ctx context.Context, signal os.Signal) { // forceCloseWebServers forced shuts down all servers. func forceCloseWebServers(ctx context.Context) { - serverMapping.RLockFunc(func(m map[string]any) { + serverMapping.RLockFunc(func(m map[string]*Server) { for _, v := range m { - for _, s := range v.(*Server).servers { + for _, s := range v.servers { s.Close(ctx) } } diff --git a/net/ghttp/ghttp_server_router_serve.go b/net/ghttp/ghttp_server_router_serve.go index b5c03fe25..63cd8b2ef 100644 --- a/net/ghttp/ghttp_server_router_serve.go +++ b/net/ghttp/ghttp_server_router_serve.go @@ -119,8 +119,8 @@ func (s *Server) searchHandlers(method, path, domain string) (parsedItems []*Han array = strings.Split(path[1:], "/") } var ( - lastMiddlewareElem *glist.Element - parsedItemList = glist.New() + lastMiddlewareElem *glist.TElement[*HandlerItemParsed] + parsedItemList = glist.NewT[*HandlerItemParsed]() repeatHandlerCheckMap = make(map[int]struct{}, 16) ) @@ -245,7 +245,7 @@ func (s *Server) searchHandlers(method, path, domain string) (parsedItems []*Han var index = 0 parsedItems = make([]*HandlerItemParsed, parsedItemList.Len()) for e := parsedItemList.Front(); e != nil; e = e.Next() { - parsedItems[index] = e.Value.(*HandlerItemParsed) + parsedItems[index] = e.Value index++ } } diff --git a/net/goai/goai.go b/net/goai/goai.go index 2705c48d4..c27152e10 100644 --- a/net/goai/goai.go +++ b/net/goai/goai.go @@ -144,24 +144,26 @@ func (oai *OpenApiV3) golangTypeToOAIType(t reflect.Type) string { for t.Kind() == reflect.Pointer { t = t.Elem() } + + switch t.String() { + case `time.Time`, `gtime.Time`: + return TypeString + case `ghttp.UploadFile`: + return TypeFile + case `[]uint8`: + return TypeString + case `uuid.UUID`: + return TypeString + } + switch t.Kind() { case reflect.String: return TypeString case reflect.Struct: - switch t.String() { - case `time.Time`, `gtime.Time`: - return TypeString - case `ghttp.UploadFile`: - return TypeFile - } return TypeObject case reflect.Slice, reflect.Array: - switch t.String() { - case `[]uint8`: - return TypeString - } return TypeArray case reflect.Bool: diff --git a/net/gtcp/gtcp_pool.go b/net/gtcp/gtcp_pool.go index 70b6ae735..6bccbdf68 100644 --- a/net/gtcp/gtcp_pool.go +++ b/net/gtcp/gtcp_pool.go @@ -30,13 +30,14 @@ const ( ) var ( + poolChecker = func(v *gpool.Pool) bool { return v == nil } // addressPoolMap is a mapping for address to its pool object. - addressPoolMap = gmap.NewStrAnyMap(true) + addressPoolMap = gmap.NewKVMapWithChecker[string, *gpool.Pool](poolChecker, true) ) // NewPoolConn creates and returns a connection with pool feature. func NewPoolConn(addr string, timeout ...time.Duration) (*PoolConn, error) { - v := addressPoolMap.GetOrSetFuncLock(addr, func() any { + v := addressPoolMap.GetOrSetFuncLock(addr, func() *gpool.Pool { var pool *gpool.Pool pool = gpool.New(defaultPoolExpire, func() (any, error) { if conn, err := NewConn(addr, timeout...); err == nil { @@ -47,7 +48,7 @@ func NewPoolConn(addr string, timeout ...time.Duration) (*PoolConn, error) { }) return pool }) - value, err := v.(*gpool.Pool).Get() + value, err := v.Get() if err != nil { return nil, err } diff --git a/net/gtcp/gtcp_server.go b/net/gtcp/gtcp_server.go index e3d8ae77b..5b1c252c1 100644 --- a/net/gtcp/gtcp_server.go +++ b/net/gtcp/gtcp_server.go @@ -38,7 +38,12 @@ type Server struct { } // Map for name to server, for singleton purpose. -var serverMapping = gmap.NewStrAnyMap(true) +var ( + // checker is used for checking whether the value is nil. + checker = func(v *Server) bool { return v == nil } + // serverMapping is the map for name to server. + serverMapping = gmap.NewKVMapWithChecker[any, *Server](checker, true) +) // GetServer returns the TCP server with specified `name`, // or it returns a new normal TCP server named `name` if it does not exist. @@ -48,9 +53,9 @@ func GetServer(name ...any) *Server { if len(name) > 0 && name[0] != "" { serverName = gconv.String(name[0]) } - return serverMapping.GetOrSetFuncLock(serverName, func() any { + return serverMapping.GetOrSetFuncLock(serverName, func() *Server { return NewServer("", nil) - }).(*Server) + }) } // NewServer creates and returns a new normal TCP server. diff --git a/net/gudp/gudp_server.go b/net/gudp/gudp_server.go index 9a23e2772..5444ed801 100644 --- a/net/gudp/gudp_server.go +++ b/net/gudp/gudp_server.go @@ -47,8 +47,10 @@ type Server struct { type ServerHandler func(conn *ServerConn) var ( + // checker is used for checking whether the value is nil. + checker = func(v *Server) bool { return v == nil } // serverMapping is used for instance name to its UDP server mappings. - serverMapping = gmap.NewStrAnyMap(true) + serverMapping = gmap.NewKVMapWithChecker[string, *Server](checker, true) ) // GetServer creates and returns an udp server instance with given name. @@ -57,12 +59,9 @@ func GetServer(name ...any) *Server { if len(name) > 0 && name[0] != "" { serverName = gconv.String(name[0]) } - if s := serverMapping.Get(serverName); s != nil { - return s.(*Server) - } - s := NewServer("", nil) - serverMapping.Set(serverName, s) - return s + return serverMapping.GetOrSetFuncLock(serverName, func() *Server { + return NewServer("", nil) + }) } // NewServer creates and returns an udp server. diff --git a/os/gcache/gcache_adapter_memory.go b/os/gcache/gcache_adapter_memory.go index 49daf7290..baf4ec922 100644 --- a/os/gcache/gcache_adapter_memory.go +++ b/os/gcache/gcache_adapter_memory.go @@ -21,12 +21,12 @@ import ( // AdapterMemory is an adapter implements using memory. type AdapterMemory struct { - data *memoryData // data is the underlying cache data which is stored in a hash table. - expireTimes *memoryExpireTimes // expireTimes is the expiring key to its timestamp mapping, which is used for quick indexing and deleting. - expireSets *memoryExpireSets // expireSets is the expiring timestamp to its key set mapping, which is used for quick indexing and deleting. - lru *memoryLru // lru is the LRU manager, which is enabled when attribute cap > 0. - eventList *glist.List // eventList is the asynchronous event list for internal data synchronization. - closed *gtype.Bool // closed controls the cache closed or not. + data *memoryData // data is the underlying cache data which is stored in a hash table. + expireTimes *memoryExpireTimes // expireTimes is the expiring key to its timestamp mapping, which is used for quick indexing and deleting. + expireSets *memoryExpireSets // expireSets is the expiring timestamp to its key set mapping, which is used for quick indexing and deleting. + lru *memoryLru // lru is the LRU manager, which is enabled when attribute cap > 0. + eventList *glist.TList[*adapterMemoryEvent] // eventList is the asynchronous event list for internal data synchronization. + closed *gtype.Bool // closed controls the cache closed or not. } var _ Adapter = (*AdapterMemory)(nil) @@ -61,7 +61,7 @@ func doNewAdapterMemory() *AdapterMemory { data: newMemoryData(), expireTimes: newMemoryExpireTimes(), expireSets: newMemoryExpireSets(), - eventList: glist.New(true), + eventList: glist.NewT[*adapterMemoryEvent](true), closed: gtype.NewBool(), } // Here may be a "timer leak" if adapter is manually changed from adapter_memory adapter. @@ -414,7 +414,6 @@ func (c *AdapterMemory) syncEventAndClearExpired(ctx context.Context) { return } var ( - event *adapterMemoryEvent oldExpireTime int64 newExpireTime int64 ) @@ -422,11 +421,10 @@ func (c *AdapterMemory) syncEventAndClearExpired(ctx context.Context) { // Data expiration synchronization. // ================================ for { - v := c.eventList.PopFront() - if v == nil { + event := c.eventList.PopFront() + if event == nil { break } - event = v.(*adapterMemoryEvent) // Fetching the old expire set. oldExpireTime = c.expireTimes.Get(event.k) // Calculating the new expiration time set. diff --git a/os/gcache/gcache_adapter_memory_lru.go b/os/gcache/gcache_adapter_memory_lru.go index 8473a88c4..d72817ad0 100644 --- a/os/gcache/gcache_adapter_memory_lru.go +++ b/os/gcache/gcache_adapter_memory_lru.go @@ -13,20 +13,23 @@ import ( "github.com/gogf/gf/v2/container/gmap" ) +// checker is used to check if the value is nil. +var checker = func(v *glist.Element) bool { return v == nil } + // memoryLru holds LRU info. // It uses list.List from stdlib for its underlying doubly linked list. type memoryLru struct { - mu sync.RWMutex // Mutex to guarantee concurrent safety. - cap int // LRU cap. - data *gmap.Map // Key mapping to the item of the list. - list *glist.List // Key list. + mu sync.RWMutex // Mutex to guarantee concurrent safety. + cap int // LRU cap. + data *gmap.KVMap[any, *glist.Element] // Key mapping to the item of the list. + list *glist.List // Key list. } // newMemoryLru creates and returns a new LRU manager. func newMemoryLru(cap int) *memoryLru { lru := &memoryLru{ cap: cap, - data: gmap.New(false), + data: gmap.NewKVMapWithChecker[any, *glist.Element](checker, false), list: glist.New(false), } return lru @@ -41,7 +44,7 @@ func (l *memoryLru) Remove(keys ...any) { defer l.mu.Unlock() for _, key := range keys { if v := l.data.Remove(key); v != nil { - l.list.Remove(v.(*glist.Element)) + l.list.Remove(v) } } } @@ -63,9 +66,8 @@ func (l *memoryLru) SaveAndEvict(keys ...any) (evictedKeys []any) { } func (l *memoryLru) doSaveAndEvict(key any) (evictedKey any) { - var element *glist.Element - if v := l.data.Get(key); v != nil { - element = v.(*glist.Element) + element := l.data.Get(key) + if element != nil { if element.Prev() == nil { // It this element is already on top of list, // it ignores the element moving. diff --git a/os/gcfg/gcfg.go b/os/gcfg/gcfg.go index 0768f8d5a..d41f8a187 100644 --- a/os/gcfg/gcfg.go +++ b/os/gcfg/gcfg.go @@ -58,7 +58,7 @@ func Instance(name ...string) *Config { if len(name) > 0 && name[0] != "" { instanceName = name[0] } - return localInstances.GetOrSetFuncLock(instanceName, func() any { + return localInstances.GetOrSetFuncLock(instanceName, func() *Config { adapterFile, err := NewAdapterFile() if err != nil { intlog.Errorf(context.Background(), `%+v`, err) @@ -68,7 +68,7 @@ func Instance(name ...string) *Config { adapterFile.SetFileName(instanceName) } return NewWithAdapter(adapterFile) - }).(*Config) + }) } // SetAdapter sets the adapter of the current Config object. diff --git a/os/gcfg/gcfg_adapter_file.go b/os/gcfg/gcfg_adapter_file.go index 64be33238..459a40967 100644 --- a/os/gcfg/gcfg_adapter_file.go +++ b/os/gcfg/gcfg_adapter_file.go @@ -46,8 +46,9 @@ const ( var ( supportedFileTypes = []string{"toml", "yaml", "yml", "json", "ini", "xml", "properties"} // All supported file types suffixes. - localInstances = gmap.NewStrAnyMap(true) // Instances map containing configuration instances. - customConfigContentMap = gmap.NewStrStrMap(true) // Customized configuration content. + checker = func(v *Config) bool { return v == nil } + localInstances = gmap.NewKVMapWithChecker[string, *Config](checker, true) // Instances map containing configuration instances. + customConfigContentMap = gmap.NewStrStrMap(true) // Customized configuration content. // Prefix array for trying searching in resource manager. resourceTryFolders = []string{ diff --git a/os/gcfg/gcfg_adapter_file_content.go b/os/gcfg/gcfg_adapter_file_content.go index 0ee43c7f1..c3bd240ff 100644 --- a/os/gcfg/gcfg_adapter_file_content.go +++ b/os/gcfg/gcfg_adapter_file_content.go @@ -20,13 +20,11 @@ func (a *AdapterFile) SetContent(content string, fileNameOrPath ...string) { usedFileNameOrPath = fileNameOrPath[0] } // Clear file cache for instances which cached `name`. - localInstances.LockFunc(func(m map[string]any) { + localInstances.LockFunc(func(m map[string]*Config) { if customConfigContentMap.Contains(usedFileNameOrPath) { for _, v := range m { - if configInstance, ok := v.(*Config); ok { - if fileConfig, ok := configInstance.GetAdapter().(*AdapterFile); ok { - fileConfig.jsonMap.Remove(usedFileNameOrPath) - } + if fileConfig, ok := v.GetAdapter().(*AdapterFile); ok { + fileConfig.jsonMap.Remove(usedFileNameOrPath) } } } @@ -54,13 +52,11 @@ func (a *AdapterFile) RemoveContent(fileNameOrPath ...string) { usedFileNameOrPath = fileNameOrPath[0] } // Clear file cache for instances which cached `name`. - localInstances.LockFunc(func(m map[string]any) { + localInstances.LockFunc(func(m map[string]*Config) { if customConfigContentMap.Contains(usedFileNameOrPath) { for _, v := range m { - if configInstance, ok := v.(*Config); ok { - if fileConfig, ok := configInstance.GetAdapter().(*AdapterFile); ok { - fileConfig.jsonMap.Remove(usedFileNameOrPath) - } + if fileConfig, ok := v.GetAdapter().(*AdapterFile); ok { + fileConfig.jsonMap.Remove(usedFileNameOrPath) } } customConfigContentMap.Remove(usedFileNameOrPath) @@ -75,12 +71,10 @@ func (a *AdapterFile) RemoveContent(fileNameOrPath ...string) { func (a *AdapterFile) ClearContent() { customConfigContentMap.Clear() // Clear cache for all instances. - localInstances.LockFunc(func(m map[string]any) { + localInstances.LockFunc(func(m map[string]*Config) { for _, v := range m { - if configInstance, ok := v.(*Config); ok { - if fileConfig, ok := configInstance.GetAdapter().(*AdapterFile); ok { - fileConfig.jsonMap.Clear() - } + if fileConfig, ok := v.GetAdapter().(*AdapterFile); ok { + fileConfig.jsonMap.Clear() } } }) diff --git a/os/gcfg/gcfg_z_unit_instance_test.go b/os/gcfg/gcfg_z_unit_instance_test.go index 47bf3cbda..b286b2cd6 100644 --- a/os/gcfg/gcfg_z_unit_instance_test.go +++ b/os/gcfg/gcfg_z_unit_instance_test.go @@ -94,7 +94,7 @@ func Test_Instance_EnvPath(t *testing.T) { t.Assert(Instance("c3") != nil, true) t.Assert(Instance("c3").MustGet(ctx, "my-config"), "3") t.Assert(Instance("c4").MustGet(ctx, "my-config"), "4") - localInstances = gmap.NewStrAnyMap(true) + localInstances = gmap.NewKVMapWithChecker[string, *Config](checker, true) }) } @@ -105,6 +105,6 @@ func Test_Instance_EnvFile(t *testing.T) { genv.Set("GF_GCFG_FILE", "c6.json") defer genv.Set("GF_GCFG_FILE", "") t.Assert(Instance().MustGet(ctx, "my-config"), "6") - localInstances = gmap.NewStrAnyMap(true) + localInstances = gmap.NewKVMapWithChecker[string, *Config](checker, true) }) } diff --git a/os/gfpool/gfpool.go b/os/gfpool/gfpool.go index 5eec01b27..b6bd61078 100644 --- a/os/gfpool/gfpool.go +++ b/os/gfpool/gfpool.go @@ -36,6 +36,8 @@ type File struct { } var ( + // checker is used for checking whether the value is nil. + checker = func(v *Pool) bool { return v == nil } // Global file pointer pool. - pools = gmap.NewStrAnyMap(true) + pools = gmap.NewKVMapWithChecker[string, *Pool](checker, true) ) diff --git a/os/gfpool/gfpool_file.go b/os/gfpool/gfpool_file.go index b77f2fbf4..b0bc98d8a 100644 --- a/os/gfpool/gfpool_file.go +++ b/os/gfpool/gfpool_file.go @@ -31,10 +31,10 @@ func Open(path string, flag int, perm os.FileMode, ttl ...time.Duration) (file * // } pool := pools.GetOrSetFuncLock( fmt.Sprintf("%s&%d&%d&%d", path, flag, fpTTL, perm), - func() any { + func() *Pool { return New(path, flag, perm, fpTTL) }, - ).(*Pool) + ) return pool.File() } @@ -52,7 +52,7 @@ func Get(path string, flag int, perm os.FileMode, ttl ...time.Duration) (file *F return nil } - fp, _ := f.(*Pool).pool.Get() + fp, _ := f.pool.Get() return fp.(*File) } diff --git a/os/gfsnotify/gfsnotify.go b/os/gfsnotify/gfsnotify.go index 2ff8f7eb0..8eb011fec 100644 --- a/os/gfsnotify/gfsnotify.go +++ b/os/gfsnotify/gfsnotify.go @@ -27,22 +27,22 @@ import ( // Watcher is the monitor for file changes. type Watcher struct { - watcher *fsnotify.Watcher // Underlying fsnotify object. - events *gqueue.Queue // Used for internal event management. - cache *gcache.Cache // Used for repeated event filter. - nameSet *gset.StrSet // Used for AddOnce feature. - callbacks *gmap.StrAnyMap // Path(file/folder) to callbacks mapping. - closeChan chan struct{} // Used for watcher closing notification. + watcher *fsnotify.Watcher // Underlying fsnotify object. + events *gqueue.TQueue[*Event] // Used for internal event management. + cache *gcache.Cache // Used for repeated event filter. + nameSet *gset.StrSet // Used for AddOnce feature. + callbacks *gmap.KVMap[string, *glist.TList[*Callback]] // Path(file/folder) to callbacks mapping. + closeChan chan struct{} // Used for watcher closing notification. } // Callback is the callback function for Watcher. type Callback struct { - Id int // Unique id for callback object. - Func func(event *Event) // Callback function. - Path string // Bound file path (absolute). - name string // Registered name for AddOnce. - elem *glist.Element // Element in the callbacks of watcher. - recursive bool // Is bound to sub-path recursively or not. + Id int // Unique id for callback object. + Func func(event *Event) // Callback function. + Path string // Bound file path (absolute). + name string // Registered name for AddOnce. + elem *glist.TElement[*Callback] // Element in the callbacks of watcher. + recursive bool // Is bound to sub-path recursively or not. } // Event is the event produced by underlying fsnotify. @@ -82,10 +82,12 @@ const ( ) var ( - mu sync.Mutex // Mutex for concurrent safety of defaultWatcher. - defaultWatcher *Watcher // Default watcher. - callbackIdMap = gmap.NewIntAnyMap(true) // Global callback id to callback function mapping. - callbackIdGenerator = gtype.NewInt() // Atomic id generator for callback. + callBacksChecker = func(v *glist.TList[*Callback]) bool { return v == nil } // callBacksChecker checks whether the value is nil. + callbackIdMapChecker = func(v *Callback) bool { return v == nil } // callbackIdMapChecker checks whether the value is nil. + mu sync.Mutex // Mutex for concurrent safety of defaultWatcher. + defaultWatcher *Watcher // Default watcher. + callbackIdMap = gmap.NewKVMapWithChecker[int, *Callback](callbackIdMapChecker, true) // Global callback id to callback function mapping. + callbackIdGenerator = gtype.NewInt() // Atomic id generator for callback. ) // New creates and returns a new watcher. @@ -96,10 +98,10 @@ var ( func New() (*Watcher, error) { w := &Watcher{ cache: gcache.New(), - events: gqueue.New(), + events: gqueue.NewTQueue[*Event](), nameSet: gset.NewStrSet(true), closeChan: make(chan struct{}), - callbacks: gmap.NewStrAnyMap(true), + callbacks: gmap.NewKVMapWithChecker[string, *glist.TList[*Callback]](callBacksChecker, true), } if watcher, err := fsnotify.NewWatcher(); err == nil { w.watcher = watcher @@ -154,11 +156,7 @@ func RemoveCallback(callbackID int) error { if err != nil { return err } - callback := (*Callback)(nil) - if r := callbackIdMap.Get(callbackID); r != nil { - callback = r.(*Callback) - } - if callback == nil { + if callback := callbackIdMap.Get(callbackID); callback == nil { return gerror.NewCodef(gcode.CodeInvalidParameter, `callback for id %d not found`, callbackID) } w.RemoveCallback(callbackID) diff --git a/os/gfsnotify/gfsnotify_watcher.go b/os/gfsnotify/gfsnotify_watcher.go index 9524d439d..b90fa7804 100644 --- a/os/gfsnotify/gfsnotify_watcher.go +++ b/os/gfsnotify/gfsnotify_watcher.go @@ -105,13 +105,11 @@ func (w *Watcher) addWithCallbackFunc(name, path string, callbackFunc func(event recursive: !watchOption.NoRecursive, } // Register the callback to watcher. - w.callbacks.LockFunc(func(m map[string]any) { - list := (*glist.List)(nil) - if v, ok := m[path]; !ok { - list = glist.New(true) + w.callbacks.LockFunc(func(m map[string]*glist.TList[*Callback]) { + list, ok := m[path] + if !ok { + list = glist.NewT[*Callback](true) m[path] = list - } else { - list = v.(*glist.List) } callback.elem = list.PushBack(callback) }) @@ -155,10 +153,9 @@ func (w *Watcher) Remove(path string) error { for _, removedPath := range removedPaths { // remove the callbacks of the path. if value := w.callbacks.Remove(removedPath); value != nil { - list := value.(*glist.List) for { - if item := list.PopFront(); item != nil { - callbackIdMap.Remove(item.(*Callback).Id) + if item := value.PopFront(); item != nil { + callbackIdMap.Remove(item.Id) } else { break } @@ -180,13 +177,9 @@ func (w *Watcher) Remove(path string) error { // // Note that, it auto removes the path watching if there's no callback bound on it. func (w *Watcher) RemoveCallback(callbackID int) { - callback := (*Callback)(nil) - if r := callbackIdMap.Get(callbackID); r != nil { - callback = r.(*Callback) - } - if callback != nil { + if callback := callbackIdMap.Get(callbackID); callback != nil { if r := w.callbacks.Get(callback.Path); r != nil { - r.(*glist.List).Remove(callback.elem) + r.Remove(callback.elem) } callbackIdMap.Remove(callbackID) if callback.name != "" { diff --git a/os/gfsnotify/gfsnotify_watcher_loop.go b/os/gfsnotify/gfsnotify_watcher_loop.go index 730931b11..e3818a5e7 100644 --- a/os/gfsnotify/gfsnotify_watcher_loop.go +++ b/os/gfsnotify/gfsnotify_watcher_loop.go @@ -9,7 +9,6 @@ package gfsnotify import ( "context" - "github.com/gogf/gf/v2/container/glist" "github.com/gogf/gf/v2/errors/gcode" "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/internal/intlog" @@ -63,69 +62,68 @@ func (w *Watcher) eventLoop() { ) for { if v := w.events.Pop(); v != nil { - event := v.(*Event) // If there's no any callback of this path, it removes it from monitor, // as a path watching without callback is meaningless. - callbacks := w.getCallbacksForPath(event.Path) + callbacks := w.getCallbacksForPath(v.Path) if len(callbacks) == 0 { - _ = w.watcher.Remove(event.Path) + _ = w.watcher.Remove(v.Path) continue } switch { - case event.IsRemove(): + case v.IsRemove(): // It should check again the existence of the path. // It adds it back to the monitor if it still exists. - if fileExists(event.Path) { + if fileExists(v.Path) { // A watch will be automatically removed if the watched path is deleted or // renamed. // // It here adds the path back to monitor. // We need no worry about the repeat adding. - if err = w.watcher.Add(event.Path); err != nil { + if err = w.watcher.Add(v.Path); err != nil { intlog.Errorf(ctx, `%+v`, err) } else { intlog.Printf( ctx, "fake remove event, watcher re-adds monitor for: %s", - event.Path, + v.Path, ) } // Change the event to RENAME, which means it renames itself to its origin name. - event.Op = RENAME + v.Op = RENAME } - case event.IsRename(): + case v.IsRename(): // It should check again the existence of the path. // It adds it back to the monitor if it still exists. // Especially Some editors might do RENAME and then CHMOD when it's editing file. - if fileExists(event.Path) { + if fileExists(v.Path) { // A watch will be automatically removed if the watched path is deleted or // renamed. // // It might lose the monitoring for the path, so we add the path back to monitor. // We need no worry about the repeat adding. - if err = w.watcher.Add(event.Path); err != nil { + if err = w.watcher.Add(v.Path); err != nil { intlog.Errorf(ctx, `%+v`, err) } else { intlog.Printf( ctx, "fake rename event, watcher re-adds monitor for: %s", - event.Path, + v.Path, ) } // Change the event to CHMOD. - event.Op = CHMOD + v.Op = CHMOD } - case event.IsCreate(): + case v.IsCreate(): // ================================================================================= // Note that it here just adds the path to monitor without any callback registering, // because its parent already has the callbacks. // ================================================================================= - if w.checkRecursiveWatchingInCreatingEvent(event.Path) { + if w.checkRecursiveWatchingInCreatingEvent(v.Path) { // It handles only folders, watching folders also watching its sub files. - for _, subPath := range fileAllDirs(event.Path) { + for _, subPath := range fileAllDirs(v.Path) { if fileIsDir(subPath) { if err = w.watcher.Add(subPath); err != nil { intlog.Errorf(ctx, `%+v`, err) @@ -142,7 +140,7 @@ func (w *Watcher) eventLoop() { } // Calling the callbacks in multiple goroutines. for _, callback := range callbacks { - go w.doCallback(event, callback) + go w.doCallback(v, callback) } } else { break @@ -166,9 +164,8 @@ func (w *Watcher) checkRecursiveWatchingInCreatingEvent(path string) bool { break } if callbackItem := w.callbacks.Get(parentDirPath); callbackItem != nil { - for _, node := range callbackItem.(*glist.List).FrontAll() { - callback := node.(*Callback) - if callback.recursive { + for _, node := range callbackItem.FrontAll() { + if node.recursive { return true } } @@ -201,10 +198,7 @@ func (w *Watcher) doCallback(event *Event, callback *Callback) { func (w *Watcher) getCallbacksForPath(path string) (callbacks []*Callback) { // Firstly add the callbacks of itself. if item := w.callbacks.Get(path); item != nil { - for _, node := range item.(*glist.List).FrontAll() { - callback := node.(*Callback) - callbacks = append(callbacks, callback) - } + callbacks = append(callbacks, item.FrontAll()...) } // ============================================================================================================ // Secondly searches its direct parent for callbacks. @@ -214,10 +208,7 @@ func (w *Watcher) getCallbacksForPath(path string) (callbacks []*Callback) { // ============================================================================================================ dirPath := fileDir(path) if item := w.callbacks.Get(dirPath); item != nil { - for _, node := range item.(*glist.List).FrontAll() { - callback := node.(*Callback) - callbacks = append(callbacks, callback) - } + callbacks = append(callbacks, item.FrontAll()...) } // Lastly searches all the parents of directory of `path` recursively for callbacks. @@ -227,10 +218,9 @@ func (w *Watcher) getCallbacksForPath(path string) (callbacks []*Callback) { break } if item := w.callbacks.Get(parentDirPath); item != nil { - for _, node := range item.(*glist.List).FrontAll() { - callback := node.(*Callback) - if callback.recursive { - callbacks = append(callbacks, callback) + for _, node := range item.FrontAll() { + if node.recursive { + callbacks = append(callbacks, node) } } } diff --git a/os/glog/glog_instance.go b/os/glog/glog_instance.go index 4f6dad14e..104308de1 100644 --- a/os/glog/glog_instance.go +++ b/os/glog/glog_instance.go @@ -14,8 +14,10 @@ const ( ) var ( + // Checker function for instances map. + checker = func(v *Logger) bool { return v == nil } // Instances map. - instances = gmap.NewStrAnyMap(true) + instances = gmap.NewKVMapWithChecker[string, *Logger](checker, true) ) // Instance returns an instance of Logger with default settings. @@ -25,7 +27,5 @@ func Instance(name ...string) *Logger { if len(name) > 0 && name[0] != "" { key = name[0] } - return instances.GetOrSetFuncLock(key, func() any { - return New() - }).(*Logger) + return instances.GetOrSetFuncLock(key, New) } diff --git a/os/gmlock/gmlock_locker.go b/os/gmlock/gmlock_locker.go index 424fcf056..0eaaa2410 100644 --- a/os/gmlock/gmlock_locker.go +++ b/os/gmlock/gmlock_locker.go @@ -12,18 +12,20 @@ import ( "github.com/gogf/gf/v2/container/gmap" ) +var checker = func(v *sync.RWMutex) bool { return v == nil } + // Locker is a memory based locker. // Note that there's no cache expire mechanism for mutex in locker. // You need remove certain mutex manually when you do not want use it anymore. type Locker struct { - m *gmap.StrAnyMap + m *gmap.KVMap[string, *sync.RWMutex] } // New creates and returns a new memory locker. // A memory locker can lock/unlock with dynamic string key. func New() *Locker { return &Locker{ - m: gmap.NewStrAnyMap(true), + m: gmap.NewKVMapWithChecker[string, *sync.RWMutex](checker, true), } } @@ -43,7 +45,7 @@ func (l *Locker) TryLock(key string) bool { // Unlock unlocks the writing lock of the `key`. func (l *Locker) Unlock(key string) { if v := l.m.Get(key); v != nil { - v.(*sync.RWMutex).Unlock() + v.Unlock() } } @@ -63,7 +65,7 @@ func (l *Locker) TryRLock(key string) bool { // RUnlock unlocks the reading lock of the `key`. func (l *Locker) RUnlock(key string) { if v := l.m.Get(key); v != nil { - v.(*sync.RWMutex).RUnlock() + v.RUnlock() } } @@ -128,7 +130,7 @@ func (l *Locker) Clear() { // getOrNewMutex returns the mutex of given `key` if it exists, // or else creates and returns a new one. func (l *Locker) getOrNewMutex(key string) *sync.RWMutex { - return l.m.GetOrSetFuncLock(key, func() any { + return l.m.GetOrSetFuncLock(key, func() *sync.RWMutex { return &sync.RWMutex{} - }).(*sync.RWMutex) + }) } diff --git a/os/gproc/gproc_comm.go b/os/gproc/gproc_comm.go index b547d31d7..9e2a3b2c1 100644 --- a/os/gproc/gproc_comm.go +++ b/os/gproc/gproc_comm.go @@ -12,6 +12,7 @@ import ( "sync" "github.com/gogf/gf/v2/container/gmap" + "github.com/gogf/gf/v2/container/gqueue" "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/internal/intlog" "github.com/gogf/gf/v2/net/gtcp" @@ -42,9 +43,11 @@ const ( ) var ( + // checker is used for checking whether the value is nil. + checker = func(v *gqueue.TQueue[*MsgRequest]) bool { return v == nil } // commReceiveQueues is the group name to queue map for storing received data. - // The value of the map is type of *gqueue.Queue. - commReceiveQueues = gmap.NewStrAnyMap(true) + // The value of the map is type of *gqueue.TQueue[*MsgRequest]. + commReceiveQueues = gmap.NewKVMapWithChecker[string, *gqueue.TQueue[*MsgRequest]](checker, true) // commPidFolderPath specifies the folder path storing pid to port mapping files. commPidFolderPath string diff --git a/os/gproc/gproc_comm_receive.go b/os/gproc/gproc_comm_receive.go index 5271b4d3d..5c8b720a3 100644 --- a/os/gproc/gproc_comm_receive.go +++ b/os/gproc/gproc_comm_receive.go @@ -39,15 +39,10 @@ func Receive(group ...string) *MsgRequest { } else { groupName = defaultGroupNameForProcComm } - queue := commReceiveQueues.GetOrSetFuncLock(groupName, func() any { - return gqueue.New(maxLengthForProcMsgQueue) - }).(*gqueue.Queue) - - // Blocking receiving. - if v := queue.Pop(); v != nil { - return v.(*MsgRequest) - } - return nil + queue := commReceiveQueues.GetOrSetFuncLock(groupName, func() *gqueue.TQueue[*MsgRequest] { + return gqueue.NewTQueue[*MsgRequest](maxLengthForProcMsgQueue) + }) + return queue.Pop() } // receiveTcpListening scans local for available port and starts listening. @@ -110,7 +105,7 @@ func receiveTcpHandler(conn *gtcp.Conn) { } else { // Push to buffer queue. response.Code = 1 - v.(*gqueue.Queue).Push(msg) + v.Push(msg) } } else { // Empty package. diff --git a/os/gres/gres_instance.go b/os/gres/gres_instance.go index 7b58c4979..71ce4baa0 100644 --- a/os/gres/gres_instance.go +++ b/os/gres/gres_instance.go @@ -14,8 +14,10 @@ const ( ) var ( + // checker checks whether the value is nil. + checker = func(v *Resource) bool { return v == nil } // Instances map. - instances = gmap.NewStrAnyMap(true) + instances = gmap.NewKVMapWithChecker[string, *Resource](checker, true) ) // Instance returns an instance of Resource. @@ -25,7 +27,5 @@ func Instance(name ...string) *Resource { if len(name) > 0 && name[0] != "" { key = name[0] } - return instances.GetOrSetFuncLock(key, func() any { - return New() - }).(*Resource) + return instances.GetOrSetFuncLock(key, New) } diff --git a/os/grpool/grpool.go b/os/grpool/grpool.go index 1abf99195..521a110fb 100644 --- a/os/grpool/grpool.go +++ b/os/grpool/grpool.go @@ -25,10 +25,10 @@ type RecoverFunc func(ctx context.Context, exception error) // Pool manages the goroutines using pool. type Pool struct { - limit int // Max goroutine count limit. - count *gtype.Int // Current running goroutine count. - list *glist.List // List for asynchronous job adding purpose. - closed *gtype.Bool // Is pool closed or not. + limit int // Max goroutine count limit. + count *gtype.Int // Current running goroutine count. + list *glist.TList[*localPoolItem] // List for asynchronous job adding purpose. + closed *gtype.Bool // Is pool closed or not. } // localPoolItem is the job item storing in job list. @@ -55,7 +55,7 @@ func New(limit ...int) *Pool { pool = &Pool{ limit: -1, count: gtype.NewInt(), - list: glist.New(true), + list: glist.NewT[*localPoolItem](true), closed: gtype.NewBool(), } timerDuration = grand.D( diff --git a/os/grpool/grpool_pool.go b/os/grpool/grpool_pool.go index 4296fc2c3..cb5d692cf 100644 --- a/os/grpool/grpool_pool.go +++ b/os/grpool/grpool_pool.go @@ -104,18 +104,12 @@ func (p *Pool) checkAndForkNewGoroutineWorker() { func (p *Pool) asynchronousWorker() { defer p.count.Add(-1) - - var ( - listItem any - poolItem *localPoolItem - ) // Harding working, one by one, job never empty, worker never die. for !p.closed.Val() { - listItem = p.list.PopBack() + listItem := p.list.PopBack() if listItem == nil { return } - poolItem = listItem.(*localPoolItem) - poolItem.Func(poolItem.Ctx) + listItem.Func(listItem.Ctx) } } diff --git a/os/gspath/gspath.go b/os/gspath/gspath.go index 051ef2f2e..5e2060614 100644 --- a/os/gspath/gspath.go +++ b/os/gspath/gspath.go @@ -39,8 +39,10 @@ type SPathCacheItem struct { } var ( + // checker is the checking function for checking the value is nil or not. + checker = func(v *SPath) bool { return v == nil } // Path to searching object mapping, used for instance management. - pathsMap = gmap.NewStrAnyMap(true) + pathsMap = gmap.NewKVMapWithChecker[string, *SPath](checker, true) ) // New creates and returns a new path searching manager. @@ -65,9 +67,9 @@ func Get(root string, cache bool) *SPath { if root == "" { root = "/" } - return pathsMap.GetOrSetFuncLock(root, func() any { + return pathsMap.GetOrSetFuncLock(root, func() *SPath { return New(root, cache) - }).(*SPath) + }) } // Search searches file `name` under path `root`. diff --git a/os/gview/gview.go b/os/gview/gview.go index acd395b48..4ab011b1a 100644 --- a/os/gview/gview.go +++ b/os/gview/gview.go @@ -23,11 +23,11 @@ import ( // View object for template engine. type View struct { - searchPaths *garray.StrArray // Searching array for path, NOT concurrent-safe for performance purpose. - data map[string]any // Global template variables. - funcMap map[string]any // Global template function map. - fileCacheMap *gmap.StrAnyMap // File cache map. - config Config // Extra configuration for the view. + searchPaths *garray.StrArray // Searching array for path, NOT concurrent-safe for performance purpose. + data map[string]any // Global template variables. + funcMap map[string]any // Global template function map. + fileCacheMap *gmap.KVMap[string, *fileCacheItem] // File cache map. + config Config // Extra configuration for the view. } type ( @@ -41,7 +41,8 @@ const ( var ( // Default view object. - defaultViewObj *View + defaultViewObj *View + fileCacheItemChecker = func(v *fileCacheItem) bool { return v == nil } ) // checkAndInitDefaultView checks and initializes the default view object. @@ -69,7 +70,7 @@ func New(path ...string) *View { searchPaths: garray.NewStrArray(), data: make(map[string]any), funcMap: make(map[string]any), - fileCacheMap: gmap.NewStrAnyMap(true), + fileCacheMap: gmap.NewKVMapWithChecker[string, *fileCacheItem](fileCacheItemChecker, true), config: DefaultConfig(), } if len(path) > 0 && len(path[0]) > 0 { diff --git a/os/gview/gview_instance.go b/os/gview/gview_instance.go index cb75d5d42..c36b0582d 100644 --- a/os/gview/gview_instance.go +++ b/os/gview/gview_instance.go @@ -14,8 +14,9 @@ const ( ) var ( + checker = func(v *View) bool { return v == nil } // Instances map. - instances = gmap.NewStrAnyMap(true) + instances = gmap.NewKVMapWithChecker[string, *View](checker, true) ) // Instance returns an instance of View with default settings. @@ -25,7 +26,7 @@ func Instance(name ...string) *View { if len(name) > 0 && name[0] != "" { key = name[0] } - return instances.GetOrSetFuncLock(key, func() any { + return instances.GetOrSetFuncLock(key, func() *View { return New() - }).(*View) + }) } diff --git a/os/gview/gview_parse.go b/os/gview/gview_parse.go index 93d151376..1f49bc33a 100644 --- a/os/gview/gview_parse.go +++ b/os/gview/gview_parse.go @@ -130,7 +130,7 @@ func (view *View) ParseWithOptions(ctx context.Context, opts Options) (result st return "", gerror.New(`template file cannot be empty`) } // It caches the file, folder, and content to enhance performance. - r := view.fileCacheMap.GetOrSetFuncLock(opts.File, func() any { + r := view.fileCacheMap.GetOrSetFuncLock(opts.File, func() *fileCacheItem { var ( path string folder string @@ -169,30 +169,29 @@ func (view *View) ParseWithOptions(ctx context.Context, opts Options) (result st if r == nil { return } - item := r.(*fileCacheItem) // It's not necessary continuing parsing if template content is empty. - if item.content == "" { + if r.content == "" { return "", nil } // If it's an Orphan option, it just parses the single file by ParseContent. if opts.Orphan { - return view.doParseContent(ctx, item.content, opts.Params) + return view.doParseContent(ctx, r.content, opts.Params) } // Get the template object instance for `folder`. var tpl any - tpl, err = view.getTemplate(item.path, item.folder, fmt.Sprintf(`*%s`, gfile.Ext(item.path))) + tpl, err = view.getTemplate(r.path, r.folder, fmt.Sprintf(`*%s`, gfile.Ext(r.path))) if err != nil { return "", err } // Using memory lock to ensure concurrent safety for template parsing. - gmlock.LockFunc("gview.Parse:"+item.path, func() { + gmlock.LockFunc("gview.Parse:"+r.path, func() { if view.config.AutoEncode { - tpl, err = tpl.(*htmltpl.Template).Parse(item.content) + tpl, err = tpl.(*htmltpl.Template).Parse(r.content) } else { - tpl, err = tpl.(*texttpl.Template).Parse(item.content) + tpl, err = tpl.(*texttpl.Template).Parse(r.content) } - if err != nil && item.path != "" { - err = gerror.Wrap(err, item.path) + if err != nil && r.path != "" { + err = gerror.Wrap(err, r.path) } }) if err != nil { diff --git a/util/gconv/gconv_scan.go b/util/gconv/gconv_scan.go index 8d8724208..9467725b8 100644 --- a/util/gconv/gconv_scan.go +++ b/util/gconv/gconv_scan.go @@ -25,3 +25,35 @@ func Scan(srcValue any, dstPointer any, paramKeyToAttrMap ...map[string]string) } return defaultConverter.Scan(srcValue, dstPointer, option) } + +// ScanWithOptions automatically checks the type of `dstPointer` and converts `srcValue` to `dstPointer`. +// It is the same as Scan function, but accepts one or more ScanOption values for additional conversion control. +// +// When using ScanWithOptions, the term "omit" means that the assignment from the source to the destination +// is skipped, so the existing value in the destination field is preserved. +// +// - option.OmitEmpty, when set to true, skips assignment of empty source values (for example: empty strings, +// zero numeric values, zero time values, empty slices or maps), preserving any existing non-empty values +// in the destination. +// +// - option.OmitNil, when set to true, skips assignment of nil source values, preserving the existing values +// in the destination when the source contains nil. +// +// Example: +// +// type User struct { +// Name string +// Email string +// } +// +// dst := &User{Name: "Alice", Email: "alice@example.com"} +// src := map[string]any{ +// "Name": "", +// "Email": nil, +// } +// +// // With OmitEmpty and OmitNil, empty and nil values in src will not overwrite dst. +// err := ScanWithOptions(src, dst, ScanOption{OmitEmpty: true, OmitNil: true}) +func ScanWithOptions(srcValue any, dstPointer any, option ...ScanOption) (err error) { + return defaultConverter.Scan(srcValue, dstPointer, option...) +} diff --git a/util/gconv/gconv_unsafe.go b/util/gconv/gconv_unsafe.go index e4b24fdfc..4381ddffc 100644 --- a/util/gconv/gconv_unsafe.go +++ b/util/gconv/gconv_unsafe.go @@ -12,12 +12,12 @@ import "unsafe" // Note that, if you completely sure you will never use `s` variable in the feature, // you can use this unsafe function to implement type conversion in high performance. func UnsafeStrToBytes(s string) []byte { - return *(*[]byte)(unsafe.Pointer(&s)) + return unsafe.Slice(unsafe.StringData(s), len(s)) } // UnsafeBytesToStr converts []byte to string without memory copy. // Note that, if you completely sure you will never use `b` variable in the feature, // you can use this unsafe function to implement type conversion in high performance. func UnsafeBytesToStr(b []byte) string { - return *(*string)(unsafe.Pointer(&b)) + return unsafe.String(unsafe.SliceData(b), len(b)) } diff --git a/util/gconv/gconv_z_unit_issue_test.go b/util/gconv/gconv_z_unit_issue_test.go index bf249383e..76443df9e 100644 --- a/util/gconv/gconv_z_unit_issue_test.go +++ b/util/gconv/gconv_z_unit_issue_test.go @@ -805,3 +805,182 @@ func Test_Issue3903(t *testing.T) { t.Assert(a.UserId, 100) }) } + +// https://github.com/gogf/gf/issues/4218 +func Test_Issue4218(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + type SysMenuVo struct { + MenuId int64 `json:"menuId" orm:"menu_id"` + MenuName string `json:"menuName" orm:"menu_name"` + Children []*SysMenuVo `json:"children" orm:"children"` + ParentId int64 `json:"parentId" orm:"parent_id"` + } + menus := []*SysMenuVo{ + { + MenuId: 1, + MenuName: "系统管理", + ParentId: 0, + }, + { + MenuId: 2, + MenuName: "字典查询", + ParentId: 1, + }, + } + var parent *SysMenuVo + err := gconv.Scan(menus[0], &parent) + t.AssertNil(err) + t.Assert(parent.MenuId, 1) + t.Assert(parent.ParentId, 0) + + parent.Children = append(parent.Children, menus[1]) + + t.Assert(len(menus[0].Children), 1) + t.Assert(menus[0].Children[0].MenuId, 2) + t.Assert(menus[0].Children[0].ParentId, 1) + }) +} + +// https://github.com/gogf/gf/issues/4542 +func Test_Issue4542(t *testing.T) { + // Test case 1: Nested map conversion - map[string]any to map[string]map[string]float64 + // This is the original bug report scenario + gtest.C(t, func(t *gtest.T) { + type ExchangeRate map[string]map[string]float64 + + // Source data from JSON unmarshalling (nested map[string]any) + source := map[string]any{ + "USD": map[string]any{ + "CNY": 7.0, + "EUR": 0.85, + }, + "EUR": map[string]any{ + "CNY": 8.2, + "USD": 1.18, + }, + } + + var exchangeRate ExchangeRate + err := gconv.Scan(source, &exchangeRate) + t.AssertNil(err) + t.Assert(len(exchangeRate), 2) + t.Assert(len(exchangeRate["USD"]), 2) + t.Assert(exchangeRate["USD"]["CNY"], 7.0) + t.Assert(exchangeRate["USD"]["EUR"], 0.85) + t.Assert(exchangeRate["EUR"]["CNY"], 8.2) + t.Assert(exchangeRate["EUR"]["USD"], 1.18) + }) + + // Test case 2: Deeply nested map conversion (3 levels) + // Verifies recursion terminates correctly at base types + gtest.C(t, func(t *gtest.T) { + type DeepMap map[string]map[string]map[string]int + + source := map[string]any{ + "level1": map[string]any{ + "level2": map[string]any{ + "level3": 100, + }, + }, + } + + var deepMap DeepMap + err := gconv.Scan(source, &deepMap) + t.AssertNil(err) + t.Assert(deepMap["level1"]["level2"]["level3"], 100) + }) + + // Test case 3: Map with different key types + gtest.C(t, func(t *gtest.T) { + source := map[string]any{ + "1": map[string]any{ + "value": 100, + }, + "2": map[string]any{ + "value": 200, + }, + } + + var result map[int]map[string]int + err := gconv.Scan(source, &result) + t.AssertNil(err) + t.Assert(result[1]["value"], 100) + t.Assert(result[2]["value"], 200) + }) + + // Test case 4: Empty nested map - verifies recursion terminates on empty map + gtest.C(t, func(t *gtest.T) { + source := map[string]any{ + "USD": map[string]any{}, + } + + var result map[string]map[string]float64 + err := gconv.Scan(source, &result) + t.AssertNil(err) + t.Assert(len(result), 1) + t.Assert(len(result["USD"]), 0) + }) + + // Test case 5: Mixed struct and map in nested structure + // Verifies struct conversion still works (no regression) + gtest.C(t, func(t *gtest.T) { + type Config struct { + Name string + Value int + } + + source := map[string]any{ + "config1": map[string]any{ + "Name": "test1", + "Value": 100, + }, + "config2": map[string]any{ + "Name": "test2", + "Value": 200, + }, + } + + // Map value is struct - should still work + var result map[string]Config + err := gconv.Scan(source, &result) + t.AssertNil(err) + t.Assert(result["config1"].Name, "test1") + t.Assert(result["config1"].Value, 100) + t.Assert(result["config2"].Name, "test2") + t.Assert(result["config2"].Value, 200) + }) + + // Test case 6: Very deep nesting (5 levels) - stress test for recursion + gtest.C(t, func(t *gtest.T) { + source := map[string]any{ + "l1": map[string]any{ + "l2": map[string]any{ + "l3": map[string]any{ + "l4": map[string]any{ + "l5": "deep_value", + }, + }, + }, + }, + } + + var result map[string]map[string]map[string]map[string]map[string]string + err := gconv.Scan(source, &result) + t.AssertNil(err) + t.Assert(result["l1"]["l2"]["l3"]["l4"]["l5"], "deep_value") + }) + + // Test case 7: Source value is not a map (should be converted first) + // Verifies no infinite recursion when source doesn't match expected structure + gtest.C(t, func(t *gtest.T) { + source := map[string]any{ + "key": "not_a_map", + } + + var result map[string]map[string]string + err := gconv.Scan(source, &result) + // This should not cause infinite recursion, but conversion may fail or return empty + // The key point is it should not hang + t.AssertNil(err) + }) +} diff --git a/util/gconv/gconv_z_unit_scan_omit_test.go b/util/gconv/gconv_z_unit_scan_omit_test.go new file mode 100644 index 000000000..8fa8e75fa --- /dev/null +++ b/util/gconv/gconv_z_unit_scan_omit_test.go @@ -0,0 +1,146 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package gconv_test + +import ( + "testing" + + "github.com/gogf/gf/v2/test/gtest" + "github.com/gogf/gf/v2/util/gconv" +) + +type User struct { + Name string + Age int + Email string +} + +type User2 struct { + Name *string + Age int + Email string +} + +type Person struct { + Name string + Age int + Email string +} + +func TestScan_OmitEmpty(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + user := User{Name: "", Age: 20, Email: ""} + person := Person{Name: "zhangsan", Age: 0, Email: "old@example.com"} + + err := gconv.ScanWithOptions(user, &person, gconv.ScanOption{ + OmitEmpty: true, + }) + t.AssertNil(err) + t.Assert(person.Name, "zhangsan") + t.Assert(person.Age, 20) + t.Assert(person.Email, "old@example.com") + }) +} + +func TestScan_AllOmitEmpty(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + user := User{Name: "", Age: 0, Email: ""} + person := Person{Name: "zhangsan", Age: 100, Email: "old@example.com"} + + err := gconv.ScanWithOptions(user, &person, gconv.ScanOption{ + OmitEmpty: true, + }) + t.AssertNil(err) + t.Assert(person.Name, "zhangsan") + t.Assert(person.Age, 100) + t.Assert(person.Email, "old@example.com") + }) +} + +func TestScan_OmitNil(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + data := map[string]any{ + "Name": nil, + "Age": 30, + "Email": nil, + } + person := Person{Name: "lisi", Age: 0, Email: "old@example.com"} + + err := gconv.ScanWithOptions(data, &person, gconv.ScanOption{ + OmitNil: true, + }) + t.AssertNil(err) + t.Assert(person.Name, "lisi") + t.Assert(person.Age, 30) + t.Assert(person.Email, "old@example.com") + }) +} + +func TestScan_OmitEmptyAndOmitNil(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + data := map[string]any{ + "Name": "", + "Age": 25, + "Email": nil, + } + person := Person{Name: "wangwu", Age: 0, Email: "old2@example.com"} + + err := gconv.ScanWithOptions(data, &person, gconv.ScanOption{ + OmitEmpty: true, + OmitNil: true, + }) + t.AssertNil(err) + t.Assert(person.Name, "wangwu") + t.Assert(person.Age, 25) + t.Assert(person.Email, "old2@example.com") + }) +} + +func TestScan_NoOmitOptions(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + user := User{Name: "", Age: 20, Email: ""} + person := Person{Name: "zhangsan", Age: 30, Email: "old@example.com"} + + err := gconv.ScanWithOptions(user, &person, gconv.ScanOption{ + OmitEmpty: false, + OmitNil: false, + }) + t.AssertNil(err) + t.Assert(person.Name, "") + t.Assert(person.Age, 20) + t.Assert(person.Email, "") + }) +} + +func TestScan_OriginalBehavior(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + user := User{Name: "newname", Age: 25, Email: "new@example.com"} + person := Person{Name: "", Age: 0, Email: ""} + + err := gconv.Scan(user, &person) + t.AssertNil(err) + t.Assert(person.Name, "newname") + t.Assert(person.Age, 25) + t.Assert(person.Email, "new@example.com") + }) +} + +func TestScan_StructOmitEmptyAndOmitNilOptions(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + user2 := User2{Name: nil, Age: 25, Email: ""} + person := Person{Name: "wangwu", Age: 0, Email: "old2@example.com"} + + err := gconv.ScanWithOptions(user2, &person, gconv.ScanOption{ + OmitEmpty: true, + OmitNil: true, + }) + t.AssertNil(err) + t.Assert(person.Name, "wangwu") + t.Assert(person.Age, 25) + t.Assert(person.Email, "old2@example.com") + }) +} diff --git a/util/gconv/internal/converter/converter_maptomap.go b/util/gconv/internal/converter/converter_maptomap.go index 6e5755cea..0f47e3db5 100644 --- a/util/gconv/internal/converter/converter_maptomap.go +++ b/util/gconv/internal/converter/converter_maptomap.go @@ -99,7 +99,12 @@ func (c *Converter) MapToMap( for _, key := range paramsKeys { mapValue := reflect.New(pointerValueType).Elem() switch pointerValueKind { - case reflect.Map, reflect.Struct: + case reflect.Map: + // For nested map types, recursively call MapToMap. + if err = c.MapToMap(paramsRv.MapIndex(key).Interface(), mapValue.Addr().Interface(), mapping, option...); err != nil { + return err + } + case reflect.Struct: structOption := StructOption{ ParamKeyToAttrMap: mapping, PriorityTag: "", diff --git a/util/gconv/internal/converter/converter_scan.go b/util/gconv/internal/converter/converter_scan.go index 6a8e8c4b4..feb7cadcb 100644 --- a/util/gconv/internal/converter/converter_scan.go +++ b/util/gconv/internal/converter/converter_scan.go @@ -23,6 +23,15 @@ type ScanOption struct { // ContinueOnError specifies whether to continue converting the next element // if one element converting fails. ContinueOnError bool + + // OmitEmpty specifies whether to skip assignment when the source value is empty + // (empty string, zero value, etc.), preserving the existing value in the + // destination field. + OmitEmpty bool + + // OmitNil specifies whether to skip assignment when the source value is nil, + // preserving the existing value in the destination field. + OmitNil bool } func (c *Converter) getScanOption(option ...ScanOption) ScanOption { @@ -87,11 +96,14 @@ func (c *Converter) Scan(srcValue any, dstPointer any, option ...ScanOption) (er } // Get the element type and kind of dstPointer - var ( - dstPointerReflectValueElem = dstPointerReflectValue.Elem() - dstPointerReflectValueElemKind = dstPointerReflectValueElem.Kind() - ) + var dstPointerReflectValueElem = dstPointerReflectValue.Elem() + // Check if srcValue and dstPointer are the same type, in which case direct assignment can be performed + if ok := c.doConvertWithTypeCheck(srcValueReflectValue, dstPointerReflectValueElem); ok { + return nil + } + // Handle multiple level pointers + var dstPointerReflectValueElemKind = dstPointerReflectValueElem.Kind() if dstPointerReflectValueElemKind == reflect.Pointer { if dstPointerReflectValueElem.IsNil() { // Create a new value for the pointer dereference @@ -105,11 +117,6 @@ func (c *Converter) Scan(srcValue any, dstPointer any, option ...ScanOption) (er return c.Scan(srcValueReflectValue, dstPointerReflectValueElem, option...) } - // Check if srcValue and dstPointer are the same type, in which case direct assignment can be performed - if ok := c.doConvertWithTypeCheck(srcValueReflectValue, dstPointerReflectValueElem); ok { - return nil - } - scanOption := c.getScanOption(option...) // Handle different destination types switch dstPointerReflectValueElemKind { @@ -292,6 +299,8 @@ func (c *Converter) doScanForComplicatedTypes( mapOption = StructOption{ ParamKeyToAttrMap: keyToAttributeNameMapping, ContinueOnError: option.ContinueOnError, + OmitEmpty: option.OmitEmpty, + OmitNil: option.OmitNil, } ) return c.Structs(srcValue, dstPointer, StructsOption{ @@ -304,6 +313,8 @@ func (c *Converter) doScanForComplicatedTypes( ParamKeyToAttrMap: keyToAttributeNameMapping, PriorityTag: "", ContinueOnError: option.ContinueOnError, + OmitEmpty: option.OmitEmpty, + OmitNil: option.OmitNil, } return c.Struct(srcValue, dstPointer, structOption) } diff --git a/util/gconv/internal/converter/converter_struct.go b/util/gconv/internal/converter/converter_struct.go index b8b10d29a..063977af6 100644 --- a/util/gconv/internal/converter/converter_struct.go +++ b/util/gconv/internal/converter/converter_struct.go @@ -30,6 +30,15 @@ type StructOption struct { // ContinueOnError specifies whether to continue converting the next element // if one element converting fails. ContinueOnError bool + + // OmitEmpty specifies whether to skip assignment when the source value is empty + // (empty string, zero value, etc.), preserving the existing value in the + // destination field. + OmitEmpty bool + + // OmitNil specifies whether to skip assignment when the source value is nil, + // preserving the existing value in the destination field. + OmitNil bool } func (c *Converter) getStructOption(option ...StructOption) StructOption { @@ -363,6 +372,13 @@ func (c *Converter) bindVarToStructField( } } }() + // Check if the value should be omitted based on OmitEmpty or OmitNil options + if option.OmitNil && empty.IsNil(srcValue) { + return nil + } + if option.OmitEmpty && empty.IsEmpty(srcValue) { + return nil + } // Directly converting. if empty.IsNil(srcValue) { fieldValue.Set(reflect.Zero(fieldValue.Type())) diff --git a/util/gutil/gutil_dump.go b/util/gutil/gutil_dump.go index 7aca81b5d..018a46986 100644 --- a/util/gutil/gutil_dump.go +++ b/util/gutil/gutil_dump.go @@ -308,8 +308,10 @@ func doDumpStruct(in doDumpInternalInput) { fmt.Fprintf(in.Buffer, ``, in.PtrAddress) return } + // Add to set and remove when function returns (path-based cycle detection). + in.DumpedPointerSet[in.PtrAddress] = struct{}{} + defer delete(in.DumpedPointerSet, in.PtrAddress) } - in.DumpedPointerSet[in.PtrAddress] = struct{}{} structFields, _ := gstructs.Fields(gstructs.FieldsInput{ Pointer: in.Value, diff --git a/util/gutil/gutil_z_unit_dump_test.go b/util/gutil/gutil_z_unit_dump_test.go index 04a966500..8ddffa8a2 100755 --- a/util/gutil/gutil_z_unit_dump_test.go +++ b/util/gutil/gutil_z_unit_dump_test.go @@ -13,6 +13,7 @@ import ( "github.com/gogf/gf/v2/container/gtype" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" + "github.com/gogf/gf/v2/os/gstructs" "github.com/gogf/gf/v2/os/gtime" "github.com/gogf/gf/v2/test/gtest" "github.com/gogf/gf/v2/text/gstr" @@ -295,3 +296,95 @@ func Test_DumpJson(t *testing.T) { gutil.DumpJson(jsonContent) }) } + +// https://github.com/gogf/gf/issues/2902 +func Test_Dump_Issue2902_SharedPointer(t *testing.T) { + type Inner struct { + Value int + } + type Outer struct { + A *Inner + B *Inner + } + gtest.C(t, func(t *gtest.T) { + // Shared pointer (not a cycle) should not be marked as cycle dump. + shared := &Inner{Value: 100} + data := Outer{A: shared, B: shared} + buffer := bytes.NewBuffer(nil) + g.DumpTo(buffer, data, gutil.DumpOption{}) + output := buffer.String() + // The second field should show the actual value, not "cycle dump". + // Both fields point to the same object, but it's not a cycle. + t.Assert(gstr.Contains(output, "cycle"), false) + t.Assert(gstr.Count(output, "Value"), 2) + t.Assert(gstr.Count(output, "100"), 2) + }) +} + +// https://github.com/gogf/gf/issues/2902 +func Test_Dump_Issue2902_SameTypeFields(t *testing.T) { + type User struct { + Id int `params:"id"` + Name int `params:"name"` + } + gtest.C(t, func(t *gtest.T) { + // Fields with same type (e.g., both are int) share the same reflect.Type, + // which should not be marked as cycle dump. + var user User + fields, _ := gstructs.TagFields(&user, []string{"p", "params"}) + buffer := bytes.NewBuffer(nil) + g.DumpTo(buffer, fields, gutil.DumpOption{}) + output := buffer.String() + // Both fields' Type should show "int", not "cycle dump". + t.Assert(gstr.Contains(output, "cycle"), false) + t.Assert(gstr.Count(output, `Type:`), 2) + }) +} + +type benchStruct struct { + A int + B string + C *benchStruct + D []int + E map[string]int +} + +func createBenchNested(depth int) *benchStruct { + if depth <= 0 { + return nil + } + return &benchStruct{ + A: depth, + B: "test", + C: createBenchNested(depth - 1), + D: []int{1, 2, 3, 4, 5}, + E: map[string]int{"x": 1, "y": 2}, + } +} + +var ( + benchShallow = &benchStruct{A: 1, B: "test", D: []int{1, 2, 3}, E: map[string]int{"a": 1}} + benchNested20 = createBenchNested(20) + benchDeep50 = createBenchNested(50) +) + +func Benchmark_Dump_Shallow(b *testing.B) { + for i := 0; i < b.N; i++ { + var buf bytes.Buffer + gutil.DumpTo(&buf, benchShallow, gutil.DumpOption{}) + } +} + +func Benchmark_Dump_Nested20(b *testing.B) { + for i := 0; i < b.N; i++ { + var buf bytes.Buffer + gutil.DumpTo(&buf, benchNested20, gutil.DumpOption{}) + } +} + +func Benchmark_Dump_Deep50(b *testing.B) { + for i := 0; i < b.N; i++ { + var buf bytes.Buffer + gutil.DumpTo(&buf, benchDeep50, gutil.DumpOption{}) + } +} diff --git a/util/gvalid/gvalid.go b/util/gvalid/gvalid.go index 534899076..0559210d2 100644 --- a/util/gvalid/gvalid.go +++ b/util/gvalid/gvalid.go @@ -91,7 +91,7 @@ var ( // The sequence tag is like: [alias@]rule[...#msg...] func ParseTagValue(tag string) (field, rule, msg string) { // Complete sequence tag. - // Example: name@required|length:2,20|password3|same:password1#||密码强度不足 | 两次密码不一致 + // Example: name@required|length:2,20|password3|same:password1#||Password strength is insufficient | Passwords are not match match, _ := gregex.MatchString(`\s*((\w+)\s*@){0,1}\s*([^#]+)\s*(#\s*(.*)){0,1}\s*`, tag) if len(match) > 5 { msg = strings.TrimSpace(match[5]) diff --git a/util/gvalid/gvalid_z_example_feature_rule_test.go b/util/gvalid/gvalid_z_example_feature_rule_test.go index f458dcc65..399c71fd7 100644 --- a/util/gvalid/gvalid_z_example_feature_rule_test.go +++ b/util/gvalid/gvalid_z_example_feature_rule_test.go @@ -954,8 +954,8 @@ func ExampleValidator_json() { var ( ctx = context.Background() req = BizReq{ - JSON1: "{\"name\":\"goframe\",\"author\":\"郭强\"}", - JSON2: "{\"name\":\"goframe\",\"author\":\"郭强\",\"test\"}", + JSON1: "{\"name\":\"goframe\",\"author\":\"Guo Qiang\"}", + JSON2: "{\"name\":\"goframe\",\"author\":\"Guo Qiang\",\"test\"}", } ) if err := g.Validator().Data(req).Run(ctx); err != nil { @@ -963,7 +963,7 @@ func ExampleValidator_json() { } // Output: - // The JSON2 value `{"name":"goframe","author":"郭强","test"}` is not a valid JSON string + // The JSON2 value `{"name":"goframe","author":"Guo Qiang","test"}` is not a valid JSON string } func ExampleValidator_integer() { diff --git a/util/gvalid/gvalid_z_example_test.go b/util/gvalid/gvalid_z_example_test.go index 52631ef77..5d56a8f8c 100644 --- a/util/gvalid/gvalid_z_example_test.go +++ b/util/gvalid/gvalid_z_example_test.go @@ -217,9 +217,9 @@ func ExampleValidator_Data_map1() { fmt.Println(e.FirstError()) } // May Output: - // map[required:账号不能为空 length:账号长度应当在 6 到 16 之间] - // passport map[required:账号不能为空 length:账号长度应当在 6 到 16 之间] - // 账号不能为空 + // map[required:The passport field is required length:The passport value `` length must be between 6 and 16] + // passport map[required:The passport field is required length:The passport value `` length must be between 6 and 16] + // The passport field is required } func ExampleValidator_Data_map2() { @@ -273,11 +273,11 @@ func ExampleValidator_Data_map3() { // May Output: // { // "passport": { - // "length": "账号长度应当在 6 到 16 之间", - // "required": "账号不能为空" + // "length": "The passport value `` length must be between 6 and 16", + // "required": "The passport field is required" // }, // "password": { - // "same": "两次密码输入不相等" + // "same": "The password value `123456` must be the same as field password2 value `1234567`" // } // } } @@ -521,5 +521,5 @@ func ExampleValidator_registerRule() { err := g.Validator().Data(user).Run(gctx.New()) fmt.Println(err.Error()) // May Output: - // 用户名称已被占用 + // The Name value `john` is not unique } diff --git a/util/gvalid/gvalid_z_unit_feature_rule_test.go b/util/gvalid/gvalid_z_unit_feature_rule_test.go index f9ffbb903..da6f94898 100755 --- a/util/gvalid/gvalid_z_unit_feature_rule_test.go +++ b/util/gvalid/gvalid_z_unit_feature_rule_test.go @@ -270,31 +270,24 @@ func Test_RequiredWithOutAll(t *testing.T) { func Test_Date(t *testing.T) { gtest.C(t, func(t *gtest.T) { - rule := "date" - val1 := "2010" - val2 := "201011" - val3 := "20101101" - val4 := "2010-11-01" - val5 := "2010.11.01" - val6 := "2010/11/01" - val7 := "2010=11=01" - val8 := "123" - err1 := g.Validator().Data(val1).Rules(rule).Run(ctx) - err2 := g.Validator().Data(val2).Rules(rule).Run(ctx) - err3 := g.Validator().Data(val3).Rules(rule).Run(ctx) - err4 := g.Validator().Data(val4).Rules(rule).Run(ctx) - err5 := g.Validator().Data(val5).Rules(rule).Run(ctx) - err6 := g.Validator().Data(val6).Rules(rule).Run(ctx) - err7 := g.Validator().Data(val7).Rules(rule).Run(ctx) - err8 := g.Validator().Data(val8).Rules(rule).Run(ctx) - t.AssertNE(err1, nil) - t.AssertNE(err2, nil) - t.Assert(err3, nil) - t.Assert(err4, nil) - t.Assert(err5, nil) - t.Assert(err6, nil) - t.AssertNE(err7, nil) - t.AssertNE(err8, nil) + m := g.MapStrBool{ + "2010": false, + "201011": false, + "20101101": true, + "2010-11-01": true, + "2010.11.01": true, + "2010/11/01": true, + "2010=11=01": false, + "123": false, + } + for k, v := range m { + err := g.Validator().Data(k).Rules("date").Run(ctx) + if v { + t.AssertNil(err) + } else { + t.AssertNE(err, nil) + } + } }) } @@ -356,232 +349,238 @@ func Test_DateFormat(t *testing.T) { func Test_Email(t *testing.T) { gtest.C(t, func(t *gtest.T) { - rule := "email" - value1 := "m@johngcn" - value2 := "m@www@johngcn" - value3 := "m-m_m@mail.johng.cn" - value4 := "m.m-m@johng.cn" - err1 := g.Validator().Data(value1).Rules(rule).Run(ctx) - err2 := g.Validator().Data(value2).Rules(rule).Run(ctx) - err3 := g.Validator().Data(value3).Rules(rule).Run(ctx) - err4 := g.Validator().Data(value4).Rules(rule).Run(ctx) - t.AssertNE(err1, nil) - t.AssertNE(err2, nil) - t.Assert(err3, nil) - t.Assert(err4, nil) + m := g.MapStrBool{ + "m@johngcn": false, + "m@www@johngcn": false, + "m-m_m@mail.johng.cn": true, + "m.m-m@johng.cn": true, + } + for k, v := range m { + err := g.Validator().Data(k).Rules("email").Run(ctx) + if v { + t.AssertNil(err) + } else { + t.AssertNE(err, nil) + } + } }) } func Test_Phone(t *testing.T) { gtest.C(t, func(t *gtest.T) { - err1 := g.Validator().Data("1361990897").Rules("phone").Run(ctx) - err2 := g.Validator().Data("13619908979").Rules("phone").Run(ctx) - err3 := g.Validator().Data("16719908979").Rules("phone").Run(ctx) - err4 := g.Validator().Data("19719908989").Rules("phone").Run(ctx) - t.AssertNE(err1.String(), nil) - t.Assert(err2, nil) - t.Assert(err3, nil) - t.Assert(err4, nil) + m := g.MapStrBool{ + "1361990897": false, + "13619908979": true, + "16719908979": true, + "19719908989": true, + } + for k, v := range m { + err := g.Validator().Data(k).Rules("phone").Run(ctx) + if v { + t.AssertNil(err) + } else { + t.AssertNE(err, nil) + } + } }) } func Test_PhoneLoose(t *testing.T) { gtest.C(t, func(t *gtest.T) { - err1 := g.Validator().Data("13333333333").Rules("phone-loose").Run(ctx) - err2 := g.Validator().Data("15555555555").Rules("phone-loose").Run(ctx) - err3 := g.Validator().Data("16666666666").Rules("phone-loose").Run(ctx) - err4 := g.Validator().Data("23333333333").Rules("phone-loose").Run(ctx) - err5 := g.Validator().Data("1333333333").Rules("phone-loose").Run(ctx) - err6 := g.Validator().Data("10333333333").Rules("phone-loose").Run(ctx) - t.Assert(err1, nil) - t.Assert(err2, nil) - t.Assert(err3, nil) - t.AssertNE(err4, nil) - t.AssertNE(err5, nil) - t.AssertNE(err6, nil) + m := g.MapStrBool{ + "13333333333": true, + "15555555555": true, + "16666666666": true, + "23333333333": false, + "1333333333": false, + "10333333333": false, + } + for k, v := range m { + err := g.Validator().Data(k).Rules("phone-loose").Run(ctx) + if v { + t.AssertNil(err) + } else { + t.AssertNE(err, nil) + } + } }) } func Test_Telephone(t *testing.T) { gtest.C(t, func(t *gtest.T) { - rule := "telephone" - val1 := "869265" - val2 := "028-869265" - val3 := "86292651" - val4 := "028-8692651" - val5 := "0830-8692651" - err1 := g.Validator().Data(val1).Rules(rule).Run(ctx) - err2 := g.Validator().Data(val2).Rules(rule).Run(ctx) - err3 := g.Validator().Data(val3).Rules(rule).Run(ctx) - err4 := g.Validator().Data(val4).Rules(rule).Run(ctx) - err5 := g.Validator().Data(val5).Rules(rule).Run(ctx) - t.AssertNE(err1, nil) - t.AssertNE(err2, nil) - t.Assert(err3, nil) - t.Assert(err4, nil) - t.Assert(err5, nil) + m := g.MapStrBool{ + "869265": false, + "028-869265": false, + "86292651": true, + "028-8692651": true, + "0830-8692651": true, + } + for k, v := range m { + err := g.Validator().Data(k).Rules("telephone").Run(ctx) + if v { + t.AssertNil(err) + } else { + t.AssertNE(err, nil) + } + } }) } func Test_Passport(t *testing.T) { gtest.C(t, func(t *gtest.T) { - rule := "passport" - val1 := "123456" - val2 := "a12345-6" - val3 := "aaaaa" - val4 := "aaaaaa" - val5 := "a123_456" - err1 := g.Validator().Data(val1).Rules(rule).Run(ctx) - err2 := g.Validator().Data(val2).Rules(rule).Run(ctx) - err3 := g.Validator().Data(val3).Rules(rule).Run(ctx) - err4 := g.Validator().Data(val4).Rules(rule).Run(ctx) - err5 := g.Validator().Data(val5).Rules(rule).Run(ctx) - t.AssertNE(err1, nil) - t.AssertNE(err2, nil) - t.AssertNE(err3, nil) - t.Assert(err4, nil) - t.Assert(err5, nil) + m := g.MapStrBool{ + "123456": false, + "a12345-6": false, + "aaaaa": false, + "aaaaaa": true, + "a123_456": true, + } + for k, v := range m { + err := g.Validator().Data(k).Rules("passport").Run(ctx) + if v { + t.AssertNil(err) + } else { + t.AssertNE(err, nil) + } + } }) } func Test_Password(t *testing.T) { gtest.C(t, func(t *gtest.T) { - rule := "password" - val1 := "12345" - val2 := "aaaaa" - val3 := "a12345-6" - val4 := ">,/;'[09-" - val5 := "a123_456" - err1 := g.Validator().Data(val1).Rules(rule).Run(ctx) - err2 := g.Validator().Data(val2).Rules(rule).Run(ctx) - err3 := g.Validator().Data(val3).Rules(rule).Run(ctx) - err4 := g.Validator().Data(val4).Rules(rule).Run(ctx) - err5 := g.Validator().Data(val5).Rules(rule).Run(ctx) - t.AssertNE(err1, nil) - t.AssertNE(err2, nil) - t.Assert(err3, nil) - t.Assert(err4, nil) - t.Assert(err5, nil) + m := g.MapStrBool{ + "12345": false, + "aaaaa": false, + "a12345-6": true, + ">,/;'[09-": true, + "a123_456": true, + } + for k, v := range m { + err := g.Validator().Data(k).Rules("password").Run(ctx) + if v { + t.AssertNil(err) + } else { + t.AssertNE(err, nil) + } + } }) } func Test_Password2(t *testing.T) { gtest.C(t, func(t *gtest.T) { - rule := "password2" - val1 := "12345" - val2 := "Naaaa" - val3 := "a12345-6" - val4 := ">,/;'[09-" - val5 := "a123_456" - val6 := "Nant1986" - val7 := "Nant1986!" - err1 := g.Validator().Data(val1).Rules(rule).Run(ctx) - err2 := g.Validator().Data(val2).Rules(rule).Run(ctx) - err3 := g.Validator().Data(val3).Rules(rule).Run(ctx) - err4 := g.Validator().Data(val4).Rules(rule).Run(ctx) - err5 := g.Validator().Data(val5).Rules(rule).Run(ctx) - err6 := g.Validator().Data(val6).Rules(rule).Run(ctx) - err7 := g.Validator().Data(val7).Rules(rule).Run(ctx) - t.AssertNE(err1, nil) - t.AssertNE(err2, nil) - t.AssertNE(err3, nil) - t.AssertNE(err4, nil) - t.AssertNE(err5, nil) - t.Assert(err6, nil) - t.Assert(err7, nil) + m := g.MapStrBool{ + "12345": false, + "Naaaa": false, + "a12345-6": false, + ">,/;'[09-": false, + "a123_456": false, + "Nant1986": true, + "Nant1986!": true, + } + for k, v := range m { + err := g.Validator().Data(k).Rules("password2").Run(ctx) + if v { + t.AssertNil(err) + } else { + t.AssertNE(err, nil) + } + } }) } func Test_Password3(t *testing.T) { gtest.C(t, func(t *gtest.T) { - rule := "password3" - val1 := "12345" - val2 := "Naaaa" - val3 := "a12345-6" - val4 := ">,/;'[09-" - val5 := "a123_456" - val6 := "Nant1986" - val7 := "Nant1986!" - err1 := g.Validator().Data(val1).Rules(rule).Run(ctx) - err2 := g.Validator().Data(val2).Rules(rule).Run(ctx) - err3 := g.Validator().Data(val3).Rules(rule).Run(ctx) - err4 := g.Validator().Data(val4).Rules(rule).Run(ctx) - err5 := g.Validator().Data(val5).Rules(rule).Run(ctx) - err6 := g.Validator().Data(val6).Rules(rule).Run(ctx) - err7 := g.Validator().Data(val7).Rules(rule).Run(ctx) - t.AssertNE(err1, nil) - t.AssertNE(err2, nil) - t.AssertNE(err3, nil) - t.AssertNE(err4, nil) - t.AssertNE(err5, nil) - t.AssertNE(err6, nil) - t.Assert(err7, nil) + m := g.MapStrBool{ + "12345": false, + "Naaaa": false, + "a12345-6": false, + ">,/;'[09-": false, + "a123_456": false, + "Nant1986": false, + "Nant1986!": true, + } + for k, v := range m { + err := g.Validator().Data(k).Rules("password3").Run(ctx) + if v { + t.AssertNil(err) + } else { + t.AssertNE(err, nil) + } + } }) } func Test_Postcode(t *testing.T) { gtest.C(t, func(t *gtest.T) { - rule := "postcode" - val1 := "12345" - val2 := "610036" - err1 := g.Validator().Data(val1).Rules(rule).Run(ctx) - err2 := g.Validator().Data(val2).Rules(rule).Run(ctx) - t.AssertNE(err1, nil) - t.Assert(err2, nil) + m := g.MapStrBool{ + "12345": false, + "610036": true, + } + for k, v := range m { + err := g.Validator().Data(k).Rules("postcode").Run(ctx) + if v { + t.AssertNil(err) + } else { + t.AssertNE(err, nil) + } + } }) } func Test_ResidentId(t *testing.T) { gtest.C(t, func(t *gtest.T) { - rule := "resident-id" - val1 := "11111111111111" - val2 := "1111111111111111" - val3 := "311128500121201" - val4 := "510521198607185367" - val5 := "51052119860718536x" - err1 := g.Validator().Data(val1).Rules(rule).Run(ctx) - err2 := g.Validator().Data(val2).Rules(rule).Run(ctx) - err3 := g.Validator().Data(val3).Rules(rule).Run(ctx) - err4 := g.Validator().Data(val4).Rules(rule).Run(ctx) - err5 := g.Validator().Data(val5).Rules(rule).Run(ctx) - t.AssertNE(err1, nil) - t.AssertNE(err2, nil) - t.AssertNE(err3, nil) - t.AssertNE(err4, nil) - t.Assert(err5, nil) + m := g.MapStrBool{ + "11111111111111": false, + "1111111111111111": false, + "311128500121201": false, + "510521198607185367": false, + "51052119860718536x": true, + } + for k, v := range m { + err := g.Validator().Data(k).Rules("resident-id").Run(ctx) + if v { + t.AssertNil(err) + } else { + t.AssertNE(err, nil) + } + } }) } func Test_BankCard(t *testing.T) { gtest.C(t, func(t *gtest.T) { - rule := "bank-card" - val1 := "6230514630000424470" - val2 := "6230514630000424473" - err1 := g.Validator().Data(val1).Rules(rule).Run(ctx) - err2 := g.Validator().Data(val2).Rules(rule).Run(ctx) - t.AssertNE(err1, nil) - t.Assert(err2, nil) + m := g.MapStrBool{ + "6230514630000424470": false, + "6230514630000424473": true, + } + for k, v := range m { + err := g.Validator().Data(k).Rules("bank-card").Run(ctx) + if v { + t.AssertNil(err) + } else { + t.AssertNE(err, nil) + } + } }) } func Test_QQ(t *testing.T) { gtest.C(t, func(t *gtest.T) { - rule := "qq" - val1 := "100" - val2 := "1" - val3 := "10000" - val4 := "38996181" - val5 := "389961817" - err1 := g.Validator().Data(val1).Rules(rule).Run(ctx) - err2 := g.Validator().Data(val2).Rules(rule).Run(ctx) - err3 := g.Validator().Data(val3).Rules(rule).Run(ctx) - err4 := g.Validator().Data(val4).Rules(rule).Run(ctx) - err5 := g.Validator().Data(val5).Rules(rule).Run(ctx) - t.AssertNE(err1, nil) - t.AssertNE(err2, nil) - t.Assert(err3, nil) - t.Assert(err4, nil) - t.Assert(err5, nil) + m := g.MapStrBool{ + "100": false, + "1": false, + "10000": true, + "38996181": true, + "389961817": true, + } + for k, v := range m { + err := g.Validator().Data(k).Rules("qq").Run(ctx) + if v { + t.AssertNil(err) + } else { + t.AssertNE(err, nil) + } + } }) } @@ -617,76 +616,78 @@ func Test_Ip(t *testing.T) { func Test_IPv4(t *testing.T) { gtest.C(t, func(t *gtest.T) { - rule := "ipv4" - val1 := "0.0.0" - val2 := "0.0.0.0" - val3 := "1.1.1.1" - val4 := "255.255.255.0" - val5 := "127.0.0.1" - err1 := g.Validator().Data(val1).Rules(rule).Run(ctx) - err2 := g.Validator().Data(val2).Rules(rule).Run(ctx) - err3 := g.Validator().Data(val3).Rules(rule).Run(ctx) - err4 := g.Validator().Data(val4).Rules(rule).Run(ctx) - err5 := g.Validator().Data(val5).Rules(rule).Run(ctx) - t.AssertNE(err1, nil) - t.Assert(err2, nil) - t.Assert(err3, nil) - t.Assert(err4, nil) - t.Assert(err5, nil) + m := g.MapStrBool{ + "0.0.0": false, + "0.0.0.0": true, + "1.1.1.1": true, + "255.255.255.0": true, + "127.0.0.1": true, + } + for k, v := range m { + err := g.Validator().Data(k).Rules("ipv4").Run(ctx) + if v { + t.AssertNil(err) + } else { + t.AssertNE(err, nil) + } + } }) } func Test_IPv6(t *testing.T) { gtest.C(t, func(t *gtest.T) { - rule := "ipv6" - val1 := "192.168.1.1" - val2 := "CDCD:910A:2222:5498:8475:1111:3900:2020" - val3 := "1030::C9B4:FF12:48AA:1A2B" - val4 := "2000:0:0:0:0:0:0:1" - val5 := "0000:0000:0000:0000:0000:ffff:c0a8:5909" - err1 := g.Validator().Data(val1).Rules(rule).Run(ctx) - err2 := g.Validator().Data(val2).Rules(rule).Run(ctx) - err3 := g.Validator().Data(val3).Rules(rule).Run(ctx) - err4 := g.Validator().Data(val4).Rules(rule).Run(ctx) - err5 := g.Validator().Data(val5).Rules(rule).Run(ctx) - t.AssertNE(err1, nil) - t.Assert(err2, nil) - t.Assert(err3, nil) - t.Assert(err4, nil) - t.Assert(err5, nil) + m := g.MapStrBool{ + "192.168.1.1": false, + "CDCD:910A:2222:5498:8475:1111:3900:2020": true, + "1030::C9B4:FF12:48AA:1A2B": true, + "2000:0:0:0:0:0:0:1": true, + "0000:0000:0000:0000:0000:ffff:c0a8:5909": true, + } + for k, v := range m { + err := g.Validator().Data(k).Rules("ipv6").Run(ctx) + if v { + t.AssertNil(err) + } else { + t.AssertNE(err, nil) + } + } }) } func Test_MAC(t *testing.T) { gtest.C(t, func(t *gtest.T) { - rule := "mac" - val1 := "192.168.1.1" - val2 := "44-45-53-54-00-00" - val3 := "01:00:5e:00:00:00" - err1 := g.Validator().Data(val1).Rules(rule).Run(ctx) - err2 := g.Validator().Data(val2).Rules(rule).Run(ctx) - err3 := g.Validator().Data(val3).Rules(rule).Run(ctx) - t.AssertNE(err1, nil) - t.Assert(err2, nil) - t.Assert(err3, nil) + m := g.MapStrBool{ + "192.168.1.1": false, + "44-45-53-54-00-00": true, + "01:00:5e:00:00:00": true, + } + for k, v := range m { + err := g.Validator().Data(k).Rules("mac").Run(ctx) + if v { + t.AssertNil(err) + } else { + t.AssertNE(err, nil) + } + } }) } func Test_URL(t *testing.T) { gtest.C(t, func(t *gtest.T) { - rule := "url" - val1 := "127.0.0.1" - val2 := "https://www.baidu.com" - val3 := "http://127.0.0.1" - val4 := "file:///tmp/test.txt" - err1 := g.Validator().Data(val1).Rules(rule).Run(ctx) - err2 := g.Validator().Data(val2).Rules(rule).Run(ctx) - err3 := g.Validator().Data(val3).Rules(rule).Run(ctx) - err4 := g.Validator().Data(val4).Rules(rule).Run(ctx) - t.AssertNE(err1, nil) - t.Assert(err2, nil) - t.Assert(err3, nil) - t.Assert(err4, nil) + m := g.MapStrBool{ + "127.0.0.1": false, + "https://www.baidu.com": true, + "http://127.0.0.1": true, + "file:///tmp/test.txt": true, + } + for k, v := range m { + err := g.Validator().Data(k).Rules("url").Run(ctx) + if v { + t.AssertNil(err) + } else { + t.AssertNE(err, nil) + } + } }) } @@ -798,290 +799,275 @@ func Test_Between(t *testing.T) { func Test_Min(t *testing.T) { gtest.C(t, func(t *gtest.T) { - rule := "min:100" - val1 := "1" - val2 := "99" - val3 := "100" - val4 := "1000" - val5 := "a" - err1 := g.Validator().Data(val1).Rules(rule).Run(ctx) - err2 := g.Validator().Data(val2).Rules(rule).Run(ctx) - err3 := g.Validator().Data(val3).Rules(rule).Run(ctx) - err4 := g.Validator().Data(val4).Rules(rule).Run(ctx) - err5 := g.Validator().Data(val5).Rules(rule).Run(ctx) - t.AssertNE(err1, nil) - t.AssertNE(err2, nil) - t.Assert(err3, nil) - t.Assert(err4, nil) - t.AssertNE(err5, nil) - - rule2 := "min:a" - err6 := g.Validator().Data(val1).Rules(rule2).Run(ctx) - t.AssertNE(err6, nil) + m := g.MapStrBool{ + "1": false, + "99": false, + "100": true, + "1000": true, + "a": false, + } + for k, v := range m { + err := g.Validator().Data(k).Rules("min:100").Run(ctx) + if v { + t.AssertNil(err) + } else { + t.AssertNE(err, nil) + } + } + }) + gtest.C(t, func(t *gtest.T) { + err := g.Validator().Data("1").Rules("min:a").Run(ctx) + t.AssertNE(err, nil) }) } func Test_Max(t *testing.T) { gtest.C(t, func(t *gtest.T) { - rule := "max:100" - val1 := "1" - val2 := "99" - val3 := "100" - val4 := "1000" - val5 := "a" - err1 := g.Validator().Data(val1).Rules(rule).Run(ctx) - err2 := g.Validator().Data(val2).Rules(rule).Run(ctx) - err3 := g.Validator().Data(val3).Rules(rule).Run(ctx) - err4 := g.Validator().Data(val4).Rules(rule).Run(ctx) - err5 := g.Validator().Data(val5).Rules(rule).Run(ctx) - t.Assert(err1, nil) - t.Assert(err2, nil) - t.Assert(err3, nil) - t.AssertNE(err4, nil) - t.AssertNE(err5, nil) - - rule2 := "max:a" - err6 := g.Validator().Data(val1).Rules(rule2).Run(ctx) - t.AssertNE(err6, nil) + m := g.MapStrBool{ + "1": true, + "99": true, + "100": true, + "1000": false, + "a": false, + } + for k, v := range m { + err := g.Validator().Data(k).Rules("max:100").Run(ctx) + if v { + t.AssertNil(err) + } else { + t.AssertNE(err, nil) + } + } + }) + gtest.C(t, func(t *gtest.T) { + err := g.Validator().Data("1").Rules("max:a").Run(ctx) + t.AssertNE(err, nil) }) } func Test_Json(t *testing.T) { gtest.C(t, func(t *gtest.T) { - rule := "json" - val1 := "" - val2 := "." - val3 := "{}" - val4 := "[]" - val5 := "[1,2,3,4]" - val6 := `{"list":[1,2,3,4]}` - err1 := g.Validator().Data(val1).Rules(rule).Run(ctx) - err2 := g.Validator().Data(val2).Rules(rule).Run(ctx) - err3 := g.Validator().Data(val3).Rules(rule).Run(ctx) - err4 := g.Validator().Data(val4).Rules(rule).Run(ctx) - err5 := g.Validator().Data(val5).Rules(rule).Run(ctx) - err6 := g.Validator().Data(val6).Rules(rule).Run(ctx) - t.AssertNE(err1, nil) - t.AssertNE(err2, nil) - t.Assert(err3, nil) - t.Assert(err4, nil) - t.Assert(err5, nil) - t.Assert(err6, nil) + m := g.MapStrBool{ + "": false, + ".": false, + "{}": true, + "[]": true, + "[1,2,3,4]": true, + `{"list":[1,2,3,4]}`: true, + } + for k, v := range m { + err := g.Validator().Data(k).Rules("json").Run(ctx) + if v { + t.AssertNil(err) + } else { + t.AssertNE(err, nil) + } + } }) } func Test_Integer(t *testing.T) { gtest.C(t, func(t *gtest.T) { - rule := "integer" - val1 := "" - val2 := "1.0" - val3 := "001" - val4 := "1" - val5 := "100" - val6 := `999999999` - err1 := g.Validator().Data(val1).Rules(rule).Run(ctx) - err2 := g.Validator().Data(val2).Rules(rule).Run(ctx) - err3 := g.Validator().Data(val3).Rules(rule).Run(ctx) - err4 := g.Validator().Data(val4).Rules(rule).Run(ctx) - err5 := g.Validator().Data(val5).Rules(rule).Run(ctx) - err6 := g.Validator().Data(val6).Rules(rule).Run(ctx) - t.AssertNE(err1, nil) - t.AssertNE(err2, nil) - t.Assert(err3, nil) - t.Assert(err4, nil) - t.Assert(err5, nil) - t.Assert(err6, nil) + m := g.MapStrBool{ + "": false, + "1.0": false, + "001": true, + "1": true, + "100": true, + "999999999": true, + } + for k, v := range m { + err := g.Validator().Data(k).Rules("integer").Run(ctx) + if v { + t.AssertNil(err) + } else { + t.AssertNE(err, nil) + } + } }) } func Test_Float(t *testing.T) { gtest.C(t, func(t *gtest.T) { - rule := "float" - val1 := "" - val2 := "a" - val3 := "1" - val4 := "1.0" - val5 := "1.1" - val6 := `0.1` - err1 := g.Validator().Data(val1).Rules(rule).Run(ctx) - err2 := g.Validator().Data(val2).Rules(rule).Run(ctx) - err3 := g.Validator().Data(val3).Rules(rule).Run(ctx) - err4 := g.Validator().Data(val4).Rules(rule).Run(ctx) - err5 := g.Validator().Data(val5).Rules(rule).Run(ctx) - err6 := g.Validator().Data(val6).Rules(rule).Run(ctx) - t.AssertNE(err1, nil) - t.AssertNE(err2, nil) - t.Assert(err3, nil) - t.Assert(err4, nil) - t.Assert(err5, nil) - t.Assert(err6, nil) + m := g.MapStrBool{ + "": false, + "a": false, + "1": true, + "1.0": true, + "1.1": true, + "0.1": true, + } + for k, v := range m { + err := g.Validator().Data(k).Rules("float").Run(ctx) + if v { + t.AssertNil(err) + } else { + t.AssertNE(err, nil) + } + } }) } func Test_Boolean(t *testing.T) { gtest.C(t, func(t *gtest.T) { - rule := "boolean" - val1 := "a" - val2 := "-" - val3 := "" - val4 := "1" - val5 := "true" - val6 := `off` - err1 := g.Validator().Data(val1).Rules(rule).Run(ctx) - err2 := g.Validator().Data(val2).Rules(rule).Run(ctx) - err3 := g.Validator().Data(val3).Rules(rule).Run(ctx) - err4 := g.Validator().Data(val4).Rules(rule).Run(ctx) - err5 := g.Validator().Data(val5).Rules(rule).Run(ctx) - err6 := g.Validator().Data(val6).Rules(rule).Run(ctx) - t.AssertNE(err1, nil) - t.AssertNE(err2, nil) - t.Assert(err3, nil) - t.Assert(err4, nil) - t.Assert(err5, nil) - t.Assert(err6, nil) + m := g.MapStrBool{ + "a": false, + "-": false, + "": true, + "1": true, + "true": true, + "off": true, + } + for k, v := range m { + err := g.Validator().Data(k).Rules("boolean").Run(ctx) + if v { + t.AssertNil(err) + } else { + t.AssertNE(err, nil) + } + } }) } func Test_Same(t *testing.T) { gtest.C(t, func(t *gtest.T) { - rule := "same:id" - val1 := "100" - params1 := g.Map{ - "age": 18, + type testCase struct { + params g.Map + pass bool } - params2 := g.Map{ - "id": 100, + cases := []testCase{ + {g.Map{"age": 18}, false}, + {g.Map{"id": 100}, true}, + {g.Map{"id": 100, "name": "john"}, true}, } - params3 := g.Map{ - "id": 100, - "name": "john", + for _, c := range cases { + err := g.Validator().Data("100").Assoc(c.params).Rules("same:id").Run(ctx) + if c.pass { + t.AssertNil(err) + } else { + t.AssertNE(err, nil) + } } - err1 := g.Validator().Data(val1).Assoc(params1).Rules(rule).Run(ctx) - err2 := g.Validator().Data(val1).Assoc(params2).Rules(rule).Run(ctx) - err3 := g.Validator().Data(val1).Assoc(params3).Rules(rule).Run(ctx) - t.AssertNE(err1, nil) - t.Assert(err2, nil) - t.Assert(err3, nil) }) } func Test_Different(t *testing.T) { gtest.C(t, func(t *gtest.T) { - rule := "different:id" - val1 := "100" - params1 := g.Map{ - "age": 18, + type testCase struct { + params g.Map + pass bool } - params2 := g.Map{ - "id": 100, + cases := []testCase{ + {g.Map{"age": 18}, true}, + {g.Map{"id": 100}, false}, + {g.Map{"id": 100, "name": "john"}, false}, } - params3 := g.Map{ - "id": 100, - "name": "john", + for _, c := range cases { + err := g.Validator().Data("100").Assoc(c.params).Rules("different:id").Run(ctx) + if c.pass { + t.AssertNil(err) + } else { + t.AssertNE(err, nil) + } } - err1 := g.Validator().Data(val1).Assoc(params1).Rules(rule).Run(ctx) - err2 := g.Validator().Data(val1).Assoc(params2).Rules(rule).Run(ctx) - err3 := g.Validator().Data(val1).Assoc(params3).Rules(rule).Run(ctx) - t.Assert(err1, nil) - t.AssertNE(err2, nil) - t.AssertNE(err3, nil) }) } func Test_EQ(t *testing.T) { gtest.C(t, func(t *gtest.T) { - rule := "eq:id" - val1 := "100" - params1 := g.Map{ - "age": 18, + type testCase struct { + params g.Map + pass bool } - params2 := g.Map{ - "id": 100, + cases := []testCase{ + {g.Map{"age": 18}, false}, + {g.Map{"id": 100}, true}, + {g.Map{"id": 100, "name": "john"}, true}, } - params3 := g.Map{ - "id": 100, - "name": "john", + for _, c := range cases { + err := g.Validator().Data("100").Assoc(c.params).Rules("eq:id").Run(ctx) + if c.pass { + t.AssertNil(err) + } else { + t.AssertNE(err, nil) + } } - err1 := g.Validator().Data(val1).Assoc(params1).Rules(rule).Run(ctx) - err2 := g.Validator().Data(val1).Assoc(params2).Rules(rule).Run(ctx) - err3 := g.Validator().Data(val1).Assoc(params3).Rules(rule).Run(ctx) - t.AssertNE(err1, nil) - t.Assert(err2, nil) - t.Assert(err3, nil) }) } func Test_Not_EQ(t *testing.T) { gtest.C(t, func(t *gtest.T) { - rule := "not-eq:id" - val1 := "100" - params1 := g.Map{ - "age": 18, + type testCase struct { + params g.Map + pass bool } - params2 := g.Map{ - "id": 100, + cases := []testCase{ + {g.Map{"age": 18}, true}, + {g.Map{"id": 100}, false}, + {g.Map{"id": 100, "name": "john"}, false}, } - params3 := g.Map{ - "id": 100, - "name": "john", + for _, c := range cases { + err := g.Validator().Data("100").Assoc(c.params).Rules("not-eq:id").Run(ctx) + if c.pass { + t.AssertNil(err) + } else { + t.AssertNE(err, nil) + } } - err1 := g.Validator().Data(val1).Assoc(params1).Rules(rule).Run(ctx) - err2 := g.Validator().Data(val1).Assoc(params2).Rules(rule).Run(ctx) - err3 := g.Validator().Data(val1).Assoc(params3).Rules(rule).Run(ctx) - t.Assert(err1, nil) - t.AssertNE(err2, nil) - t.AssertNE(err3, nil) }) } func Test_In(t *testing.T) { gtest.C(t, func(t *gtest.T) { - rule := "in:100,200" - val1 := "" - val2 := "1" - val3 := "100" - val4 := "200" - err1 := g.Validator().Data(val1).Rules(rule).Run(ctx) - err2 := g.Validator().Data(val2).Rules(rule).Run(ctx) - err3 := g.Validator().Data(val3).Rules(rule).Run(ctx) - err4 := g.Validator().Data(val4).Rules(rule).Run(ctx) - t.AssertNE(err1, nil) - t.AssertNE(err2, nil) - t.Assert(err3, nil) - t.Assert(err4, nil) + m := g.MapStrBool{ + "": false, + "1": false, + "100": true, + "200": true, + } + for k, v := range m { + err := g.Validator().Data(k).Rules("in:100,200").Run(ctx) + if v { + t.AssertNil(err) + } else { + t.AssertNE(err, nil) + } + } }) } func Test_NotIn(t *testing.T) { gtest.C(t, func(t *gtest.T) { - rule := "not-in:100" - val1 := "" - val2 := "1" - val3 := "100" - val4 := "200" - err1 := g.Validator().Data(val1).Rules(rule).Run(ctx) - err2 := g.Validator().Data(val2).Rules(rule).Run(ctx) - err3 := g.Validator().Data(val3).Rules(rule).Run(ctx) - err4 := g.Validator().Data(val4).Rules(rule).Run(ctx) - t.Assert(err1, nil) - t.Assert(err2, nil) - t.AssertNE(err3, nil) - t.Assert(err4, nil) + m := g.MapStrBool{ + "": true, + "1": true, + "100": false, + "200": true, + } + for k, v := range m { + err := g.Validator().Data(k).Rules("not-in:100").Run(ctx) + if v { + t.AssertNil(err) + } else { + t.AssertNE(err, nil) + } + } }) gtest.C(t, func(t *gtest.T) { - rule := "not-in:100,200" - val1 := "" - val2 := "1" - val3 := "100" - val4 := "200" - err1 := g.Validator().Data(val1).Rules(rule).Run(ctx) - err2 := g.Validator().Data(val2).Rules(rule).Run(ctx) - err3 := g.Validator().Data(val3).Rules(rule).Run(ctx) - err4 := g.Validator().Data(val4).Rules(rule).Run(ctx) - t.Assert(err1, nil) - t.Assert(err2, nil) - t.AssertNE(err3, nil) - t.AssertNE(err4, nil) + m := g.MapStrBool{ + "": true, + "1": true, + "100": false, + "200": false, + } + for k, v := range m { + err := g.Validator().Data(k).Rules("not-in:100,200").Run(ctx) + if v { + t.AssertNil(err) + } else { + t.AssertNE(err, nil) + } + } }) } @@ -1619,3 +1605,148 @@ func Test_Enums(t *testing.T) { t.AssertNil(err) }) } +func Test_Alpha(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + m := g.MapStrBool{ + "abc": true, + "ABC": true, + "abcABC": true, + "abc123": false, + "abc-123": false, + "abc_123": false, + "123": false, + "": false, + "abc def": false, + } + for k, v := range m { + err := g.Validator().Data(k).Rules("alpha").Run(ctx) + if v { + t.AssertNil(err) + } else { + t.AssertNE(err, nil) + } + } + }) +} + +func Test_AlphaDash(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + m := g.MapStrBool{ + "abc": true, + "ABC": true, + "abc123": true, + "abc-123": true, + "abc_123": true, + "abc-_123": true, + "abc-_ABC-123": true, + "abc 123": false, + "abc@123": false, + "": false, + } + for k, v := range m { + err := g.Validator().Data(k).Rules("alpha-dash").Run(ctx) + if v { + t.AssertNil(err) + } else { + t.AssertNE(err, nil) + } + } + }) +} + +func Test_AlphaNum(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + m := g.MapStrBool{ + "abc": true, + "ABC": true, + "123": true, + "abc123": true, + "ABC123": true, + "abcABC123": true, + "abc-123": false, + "abc_123": false, + "abc 123": false, + "": false, + } + for k, v := range m { + err := g.Validator().Data(k).Rules("alpha-num").Run(ctx) + if v { + t.AssertNil(err) + } else { + t.AssertNE(err, nil) + } + } + }) +} + +func Test_Lowercase(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + m := g.MapStrBool{ + "abc": true, + "abcdef": true, + "ABC": false, + "Abc": false, + "aBc": false, + "abc123": false, + "abc-def": false, + "abc_def": false, + "": false, + } + for k, v := range m { + err := g.Validator().Data(k).Rules("lowercase").Run(ctx) + if v { + t.AssertNil(err) + } else { + t.AssertNE(err, nil) + } + } + }) +} + +func Test_Numeric(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + m := g.MapStrBool{ + "0": true, + "123": true, + "0123": true, + "123456789": true, + "1.23": false, + "abc": false, + "123abc": false, + "abc123": false, + "": false, + } + for k, v := range m { + err := g.Validator().Data(k).Rules("numeric").Run(ctx) + if v { + t.AssertNil(err) + } else { + t.AssertNE(err, nil) + } + } + }) +} + +func Test_Uppercase(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + m := g.MapStrBool{ + "ABC": true, + "ABCDEF": true, + "abc": false, + "Abc": false, + "AbC": false, + "ABC123": false, + "ABC-DEF": false, + "ABC_DEF": false, + "": false, + } + for k, v := range m { + err := g.Validator().Data(k).Rules("uppercase").Run(ctx) + if v { + t.AssertNil(err) + } else { + t.AssertNE(err, nil) + } + } + }) +} diff --git a/util/gvalid/internal/builtin/builtin_alpha.go b/util/gvalid/internal/builtin/builtin_alpha.go new file mode 100644 index 000000000..ffddf5aad --- /dev/null +++ b/util/gvalid/internal/builtin/builtin_alpha.go @@ -0,0 +1,39 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package builtin + +import ( + "errors" + + "github.com/gogf/gf/v2/text/gregex" +) + +// RuleAlpha implements `alpha` rule: +// Alpha characters (a-z, A-Z). +// +// Format: alpha +type RuleAlpha struct{} + +func init() { + Register(RuleAlpha{}) +} + +func (r RuleAlpha) Name() string { + return "alpha" +} + +func (r RuleAlpha) Message() string { + return "The {field} value `{value}` must contain only alphabetic characters" +} + +func (r RuleAlpha) Run(in RunInput) error { + ok := gregex.IsMatchString(`^[a-zA-Z]+$`, in.Value.String()) + if ok { + return nil + } + return errors.New(in.Message) +} diff --git a/util/gvalid/internal/builtin/builtin_alpha_dash.go b/util/gvalid/internal/builtin/builtin_alpha_dash.go new file mode 100644 index 000000000..c5cb7101d --- /dev/null +++ b/util/gvalid/internal/builtin/builtin_alpha_dash.go @@ -0,0 +1,39 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package builtin + +import ( + "errors" + + "github.com/gogf/gf/v2/text/gregex" +) + +// RuleAlphaDash implements `alpha-dash` rule: +// Alpha-numeric characters, hyphens, and underscores (a-z, A-Z, 0-9, -, _). +// +// Format: alpha-dash +type RuleAlphaDash struct{} + +func init() { + Register(RuleAlphaDash{}) +} + +func (r RuleAlphaDash) Name() string { + return "alpha-dash" +} + +func (r RuleAlphaDash) Message() string { + return "The {field} value `{value}` must contain only alpha-numeric characters, hyphens, and underscores" +} + +func (r RuleAlphaDash) Run(in RunInput) error { + ok := gregex.IsMatchString(`^[a-zA-Z0-9_\-]+$`, in.Value.String()) + if ok { + return nil + } + return errors.New(in.Message) +} diff --git a/util/gvalid/internal/builtin/builtin_alpha_num.go b/util/gvalid/internal/builtin/builtin_alpha_num.go new file mode 100644 index 000000000..4599bc913 --- /dev/null +++ b/util/gvalid/internal/builtin/builtin_alpha_num.go @@ -0,0 +1,39 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package builtin + +import ( + "errors" + + "github.com/gogf/gf/v2/text/gregex" +) + +// RuleAlphaNum implements `alpha-num` rule: +// Alpha-numeric characters (a-z, A-Z, 0-9). +// +// Format: alpha-num +type RuleAlphaNum struct{} + +func init() { + Register(RuleAlphaNum{}) +} + +func (r RuleAlphaNum) Name() string { + return "alpha-num" +} + +func (r RuleAlphaNum) Message() string { + return "The {field} value `{value}` must contain only alpha-numeric characters" +} + +func (r RuleAlphaNum) Run(in RunInput) error { + ok := gregex.IsMatchString(`^[a-zA-Z0-9]+$`, in.Value.String()) + if ok { + return nil + } + return errors.New(in.Message) +} diff --git a/util/gvalid/internal/builtin/builtin_lowercase.go b/util/gvalid/internal/builtin/builtin_lowercase.go new file mode 100644 index 000000000..0e2f1a87d --- /dev/null +++ b/util/gvalid/internal/builtin/builtin_lowercase.go @@ -0,0 +1,39 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package builtin + +import ( + "errors" + + "github.com/gogf/gf/v2/text/gregex" +) + +// RuleLowercase implements `lowercase` rule: +// Lowercase alphabetic characters (a-z). +// +// Format: lowercase +type RuleLowercase struct{} + +func init() { + Register(RuleLowercase{}) +} + +func (r RuleLowercase) Name() string { + return "lowercase" +} + +func (r RuleLowercase) Message() string { + return "The {field} value `{value}` must be lowercase" +} + +func (r RuleLowercase) Run(in RunInput) error { + ok := gregex.IsMatchString(`^[a-z]+$`, in.Value.String()) + if ok { + return nil + } + return errors.New(in.Message) +} diff --git a/util/gvalid/internal/builtin/builtin_numeric.go b/util/gvalid/internal/builtin/builtin_numeric.go new file mode 100644 index 000000000..372867bd0 --- /dev/null +++ b/util/gvalid/internal/builtin/builtin_numeric.go @@ -0,0 +1,39 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package builtin + +import ( + "errors" + + "github.com/gogf/gf/v2/text/gregex" +) + +// RuleNumeric implements `numeric` rule: +// Numeric string (0-9). +// +// Format: numeric +type RuleNumeric struct{} + +func init() { + Register(RuleNumeric{}) +} + +func (r RuleNumeric) Name() string { + return "numeric" +} + +func (r RuleNumeric) Message() string { + return "The {field} value `{value}` must be numeric" +} + +func (r RuleNumeric) Run(in RunInput) error { + ok := gregex.IsMatchString(`^[0-9]+$`, in.Value.String()) + if ok { + return nil + } + return errors.New(in.Message) +} diff --git a/util/gvalid/internal/builtin/builtin_phone_loose.go b/util/gvalid/internal/builtin/builtin_phone_loose.go index 4c8bcf3ed..aedb622ae 100644 --- a/util/gvalid/internal/builtin/builtin_phone_loose.go +++ b/util/gvalid/internal/builtin/builtin_phone_loose.go @@ -13,10 +13,10 @@ import ( ) // RulePhoneLoose implements `phone-loose` rule: -// Loose mobile phone number verification(宽松的手机号验证) +// Loose mobile phone number verification. // As long as the 11 digits numbers beginning with // 13, 14, 15, 16, 17, 18, 19 can pass the verification -// (只要满足 13、14、15、16、17、18、19开头的11位数字都可以通过验证). +// (Any 11-digit numbers starting with 13, 14, 15, 16, 17, 18, 19 can pass the validation). // // Format: phone-loose type RulePhoneLoose struct{} diff --git a/util/gvalid/internal/builtin/builtin_resident_id.go b/util/gvalid/internal/builtin/builtin_resident_id.go index 3da02e786..990496abf 100644 --- a/util/gvalid/internal/builtin/builtin_resident_id.go +++ b/util/gvalid/internal/builtin/builtin_resident_id.go @@ -41,23 +41,23 @@ func (r RuleResidentId) Run(in RunInput) error { // checkResidentId checks whether given id a china resident id number. // -// xxxxxx yyyy MM dd 375 0 十八位 -// xxxxxx yy MM dd 75 0 十五位 +// xxxxxx yyyy MM dd 375 0 18 digits +// xxxxxx yy MM dd 75 0 15 digits // -// 地区: [1-9]\d{5} -// 年的前两位:(18|19|([23]\d)) 1800-2399 -// 年的后两位:\d{2} -// 月份: ((0[1-9])|(10|11|12)) -// 天数: (([0-2][1-9])|10|20|30|31) 闰年不能禁止29+ +// Region: [1-9]\d{5} +// First two digits of year: (18|19|([23]\d)) 1800-2399 +// Last two digits of year: \d{2} +// Month: ((0[1-9])|(10|11|12)) +// Day: (([0-2][1-9])|10|20|30|31) Leap year cannot prohibit 29+ // -// 三位顺序码:\d{3} -// 两位顺序码:\d{2} -// 校验码: [0-9Xx] +// Three sequential digits: \d{3} +// Two sequential digits: \d{2} +// Check code: [0-9Xx] // -// 十八位:^[1-9]\d{5}(18|19|([23]\d))\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$ -// 十五位:^[1-9]\d{5}\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{3}$ +// 18 digits: ^[1-9]\d{5}(18|19|([23]\d))\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$ +// 15 digits: ^[1-9]\d{5}\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{3}$ // -// 总: +// Total: // (^[1-9]\d{5}(18|19|([23]\d))\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$)|(^[1-9]\d{5}\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{3}$) func (r RuleResidentId) checkResidentId(id string) bool { id = strings.ToUpper(strings.TrimSpace(id)) diff --git a/util/gvalid/internal/builtin/builtin_uppercase.go b/util/gvalid/internal/builtin/builtin_uppercase.go new file mode 100644 index 000000000..0bc00da86 --- /dev/null +++ b/util/gvalid/internal/builtin/builtin_uppercase.go @@ -0,0 +1,39 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package builtin + +import ( + "errors" + + "github.com/gogf/gf/v2/text/gregex" +) + +// RuleUppercase implements `uppercase` rule: +// Uppercase alphabetic characters (A-Z). +// +// Format: uppercase +type RuleUppercase struct{} + +func init() { + Register(RuleUppercase{}) +} + +func (r RuleUppercase) Name() string { + return "uppercase" +} + +func (r RuleUppercase) Message() string { + return "The {field} value `{value}` must be uppercase" +} + +func (r RuleUppercase) Run(in RunInput) error { + ok := gregex.IsMatchString(`^[A-Z]+$`, in.Value.String()) + if ok { + return nil + } + return errors.New(in.Message) +} diff --git a/util/gvalid/testdata/i18n/cn/validation.toml b/util/gvalid/testdata/i18n/cn/validation.toml index 5b601158f..42bebe6fa 100644 --- a/util/gvalid/testdata/i18n/cn/validation.toml +++ b/util/gvalid/testdata/i18n/cn/validation.toml @@ -7,7 +7,7 @@ "gf.gvalid.rule.required-without-all" = "{field}字段不能为空" "gf.gvalid.rule.date" = "{field}字段值`{value}`日期格式不满足Y-m-d格式,例如: 2001-02-03" "gf.gvalid.rule.datetime" = "{field}字段值`{value}`日期格式不满足Y-m-d H:i:s格式,例如: 2001-02-03 12:00:00" -"gf.gvalid.rule.date-format" = "{field}字段值`{value}`日期格式不满足{format}" +"gf.gvalid.rule.date-format" = "{field}字段值`{value}`日期格式不满足{pattern}" "gf.gvalid.rule.email" = "{field}字段值`{value}`邮箱地址格式不正确" "gf.gvalid.rule.phone" = "{field}字段值`{value}`手机号码格式不正确" "gf.gvalid.rule.phone-loose" = "{field}字段值`{value}`手机号码格式不正确" diff --git a/version.go b/version.go index 6bc17b060..0d52ed59e 100644 --- a/version.go +++ b/version.go @@ -2,5 +2,5 @@ package gf const ( // VERSION is the current GoFrame version. - VERSION = "v2.9.7" + VERSION = "v2.9.8" )