mirror of
https://gitee.com/johng/gf
synced 2026-06-08 02:27:42 +08:00
Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c9641ea115 | |||
| d8a173d9f0 | |||
| 5d1712b4ab | |||
| a4f98c2490 | |||
| d1cd30c9b4 | |||
| 5979261584 | |||
| df463d75bc | |||
| de9d3c2b3c | |||
| ce3599a672 | |||
| cd6fd247e2 | |||
| be91c4889e | |||
| 6219da7a76 | |||
| c600f3aae8 | |||
| 9dd43cd331 | |||
| 3e73e2d2cc | |||
| 1ed4e0267a | |||
| 3120a8bc22 | |||
| 13524a36bc | |||
| cb26931378 | |||
| 40f4d9f8ec | |||
| caea7ea4b8 | |||
| a6485d53af | |||
| db9f47d942 | |||
| c5778127b1 | |||
| 8f826edc43 | |||
| d148e0ea62 | |||
| d091547212 | |||
| 6334ee1958 | |||
| 24939eb0d6 |
785
.github/workflows/scripts/docker-services.sh
vendored
Executable file
785
.github/workflows/scripts/docker-services.sh
vendored
Executable file
@ -0,0 +1,785 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# GoFrame Docker Services Manager
|
||||
# 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'
|
||||
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"
|
||||
|
||||
SERVICES["redis"]="redis:7.0"
|
||||
SERVICE_PORTS["redis"]="6379:6379"
|
||||
SERVICE_OPTS["redis"]="--health-cmd 'redis-cli ping' --health-interval 10s --health-timeout 5s --health-retries 5"
|
||||
|
||||
SERVICES["mysql"]="mysql:5.7"
|
||||
SERVICE_PORTS["mysql"]="3306:3306"
|
||||
SERVICE_ENVS["mysql"]="-e MYSQL_DATABASE=test -e MYSQL_ROOT_PASSWORD=12345678"
|
||||
|
||||
SERVICES["mariadb"]="mariadb:11.4"
|
||||
SERVICE_PORTS["mariadb"]="3307:3306"
|
||||
SERVICE_ENVS["mariadb"]="-e MARIADB_DATABASE=test -e MARIADB_ROOT_PASSWORD=12345678"
|
||||
|
||||
SERVICES["postgres"]="postgres:17-alpine"
|
||||
SERVICE_PORTS["postgres"]="5432:5432"
|
||||
SERVICE_ENVS["postgres"]="-e POSTGRES_PASSWORD=12345678 -e POSTGRES_USER=postgres -e POSTGRES_DB=test -e TZ=Asia/Shanghai"
|
||||
SERVICE_OPTS["postgres"]="--health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5"
|
||||
|
||||
SERVICES["mssql"]="mcr.microsoft.com/mssql/server:2022-latest"
|
||||
SERVICE_PORTS["mssql"]="1433:1433"
|
||||
SERVICE_ENVS["mssql"]="-e TZ=Asia/Shanghai -e ACCEPT_EULA=Y -e MSSQL_SA_PASSWORD=LoremIpsum86"
|
||||
|
||||
SERVICES["clickhouse"]="clickhouse/clickhouse-server:24.11.1.2557-alpine"
|
||||
SERVICE_PORTS["clickhouse"]="9000:9000 -p 8123:8123 -p 9001:9001"
|
||||
|
||||
SERVICES["polaris"]="polarismesh/polaris-standalone:v1.17.2"
|
||||
SERVICE_PORTS["polaris"]="8090:8090 -p 8091:8091 -p 8093:8093 -p 9090:9090 -p 9091:9091"
|
||||
|
||||
SERVICES["oracle"]="loads/oracle-xe-11g-r2:11.2.0"
|
||||
SERVICE_PORTS["oracle"]="1521:1521"
|
||||
SERVICE_ENVS["oracle"]="-e ORACLE_ALLOW_REMOTE=true -e ORACLE_SID=XE -e ORACLE_DB_USER_NAME=system -e ORACLE_DB_PASSWORD=oracle"
|
||||
|
||||
SERVICES["dm"]="loads/dm:v8.1.2.128_ent_x86_64_ctm_pack4"
|
||||
SERVICE_PORTS["dm"]="5236:5236"
|
||||
|
||||
SERVICES["gaussdb"]="opengauss/opengauss:7.0.0-RC1.B023"
|
||||
SERVICE_PORTS["gaussdb"]="9950:5432"
|
||||
SERVICE_ENVS["gaussdb"]="-e GS_PASSWORD=UTpass@1234 -e TZ=Asia/Shanghai"
|
||||
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"
|
||||
}
|
||||
|
||||
print_success() {
|
||||
echo -e "${GREEN}[OK]${NC} $1"
|
||||
}
|
||||
|
||||
print_warning() {
|
||||
echo -e "${YELLOW}[WARN]${NC} $1"
|
||||
}
|
||||
|
||||
print_error() {
|
||||
echo -e "${RED}[ERROR]${NC} $1"
|
||||
}
|
||||
|
||||
# Check if Docker is available
|
||||
check_docker() {
|
||||
if ! command -v docker &> /dev/null; then
|
||||
print_error "Docker is not installed or not in PATH"
|
||||
exit 1
|
||||
fi
|
||||
if ! docker info &> /dev/null; then
|
||||
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")
|
||||
local image="${SERVICES[$service]}"
|
||||
local ports="${SERVICE_PORTS[$service]}"
|
||||
local envs="${SERVICE_ENVS[$service]}"
|
||||
local opts="${SERVICE_OPTS[$service]}"
|
||||
|
||||
if [ -z "$image" ]; then
|
||||
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 is already running"
|
||||
return 0
|
||||
else
|
||||
print_info "Starting existing container $service..."
|
||||
docker start "$container_name" > /dev/null
|
||||
print_success "$service started"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
print_info "Starting $service..."
|
||||
|
||||
# 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
|
||||
|
||||
cmd="$cmd $image"
|
||||
|
||||
if eval "$cmd" > /dev/null 2>&1; then
|
||||
print_success "$service started (container: $container_name)"
|
||||
else
|
||||
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 "Stopping $service..."
|
||||
docker stop "$container_name" > /dev/null
|
||||
print_success "$service stopped"
|
||||
else
|
||||
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 "Removing $service..."
|
||||
docker rm -f "$container_name" > /dev/null
|
||||
print_success "$service removed"
|
||||
else
|
||||
print_warning "$service container does not exist"
|
||||
fi
|
||||
}
|
||||
|
||||
# View service logs
|
||||
logs_service() {
|
||||
local service=$1
|
||||
local container_name=$(get_container_name "$service")
|
||||
local lines=${2:-100}
|
||||
|
||||
if docker ps -a --format '{{.Names}}' | grep -q "^${container_name}$"; then
|
||||
docker logs --tail "$lines" -f "$container_name"
|
||||
else
|
||||
print_error "$service container does not exist"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Start docker-compose service
|
||||
start_compose_service() {
|
||||
local service=$1
|
||||
local compose_file=""
|
||||
|
||||
case $service in
|
||||
apollo)
|
||||
compose_file="$WORKFLOW_DIR/apollo/docker-compose.yml"
|
||||
;;
|
||||
nacos)
|
||||
compose_file="$WORKFLOW_DIR/nacos/docker-compose.yml"
|
||||
;;
|
||||
redis-cluster)
|
||||
compose_file="$WORKFLOW_DIR/redis/docker-compose.yml"
|
||||
;;
|
||||
consul)
|
||||
compose_file="$WORKFLOW_DIR/consul/docker-compose.yml"
|
||||
;;
|
||||
*)
|
||||
print_error "Unknown compose service: $service"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ -f "$compose_file" ]; then
|
||||
print_info "Starting $service (docker-compose)..."
|
||||
docker compose -f "$compose_file" up -d
|
||||
print_success "$service started"
|
||||
else
|
||||
print_error "Compose file does not exist: $compose_file"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Stop docker-compose service
|
||||
stop_compose_service() {
|
||||
local service=$1
|
||||
local compose_file=""
|
||||
|
||||
case $service in
|
||||
apollo)
|
||||
compose_file="$WORKFLOW_DIR/apollo/docker-compose.yml"
|
||||
;;
|
||||
nacos)
|
||||
compose_file="$WORKFLOW_DIR/nacos/docker-compose.yml"
|
||||
;;
|
||||
redis-cluster)
|
||||
compose_file="$WORKFLOW_DIR/redis/docker-compose.yml"
|
||||
;;
|
||||
consul)
|
||||
compose_file="$WORKFLOW_DIR/consul/docker-compose.yml"
|
||||
;;
|
||||
*)
|
||||
print_error "Unknown compose service: $service"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ -f "$compose_file" ]; then
|
||||
print_info "Stopping $service (docker-compose)..."
|
||||
docker compose -f "$compose_file" down
|
||||
print_success "$service stopped"
|
||||
else
|
||||
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}"
|
||||
echo ""
|
||||
printf "%-15s %-12s %-30s %s\n" "SERVICE" "STATUS" "CONTAINER" "PORTS"
|
||||
echo "--------------------------------------------------------------------------------"
|
||||
|
||||
for service in $GROUP_ALL; do
|
||||
local container_name=$(get_container_name "$service")
|
||||
local status="stopped"
|
||||
local ports="-"
|
||||
|
||||
if docker ps --format '{{.Names}}' 2>/dev/null | grep -q "^${container_name}$"; then
|
||||
status="${GREEN}running${NC}"
|
||||
ports=$(docker port "$container_name" 2>/dev/null | tr '\n' ' ' || echo "-")
|
||||
elif docker ps -a --format '{{.Names}}' 2>/dev/null | grep -q "^${container_name}$"; then
|
||||
status="${YELLOW}stopped${NC}"
|
||||
else
|
||||
status="${RED}not created${NC}"
|
||||
fi
|
||||
|
||||
printf "%-15s %-22b %-30s %s\n" "$service" "$status" "$container_name" "$ports"
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo -e "${CYAN}========== Compose Services ==========${NC}"
|
||||
echo ""
|
||||
|
||||
for compose_svc in apollo nacos redis-cluster consul; do
|
||||
local running=0
|
||||
case $compose_svc in
|
||||
apollo)
|
||||
running=$(docker ps --filter "name=apollo" --format '{{.Names}}' 2>/dev/null | wc -l)
|
||||
;;
|
||||
nacos)
|
||||
running=$(docker ps --filter "name=nacos" --format '{{.Names}}' 2>/dev/null | wc -l)
|
||||
;;
|
||||
redis-cluster)
|
||||
running=$(docker ps --filter "name=redis-" --format '{{.Names}}' 2>/dev/null | wc -l)
|
||||
;;
|
||||
consul)
|
||||
running=$(docker ps --filter "name=consul" --format '{{.Names}}' 2>/dev/null | wc -l)
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ "$running" -gt 0 ]; then
|
||||
printf "%-15s ${GREEN}running${NC} (%d containers)\n" "$compose_svc" "$running"
|
||||
else
|
||||
printf "%-15s ${RED}stopped${NC}\n" "$compose_svc"
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
}
|
||||
|
||||
# Show service information
|
||||
show_service_info() {
|
||||
echo ""
|
||||
echo -e "${CYAN}========== Available Services ==========${NC}"
|
||||
echo ""
|
||||
echo -e "${YELLOW}Basic Services (standalone containers):${NC}"
|
||||
echo ""
|
||||
printf "%-15s %-50s %s\n" "SERVICE" "IMAGE" "PORTS"
|
||||
echo "--------------------------------------------------------------------------------"
|
||||
|
||||
for service in $GROUP_ALL; do
|
||||
printf "%-15s %-50s %s\n" "$service" "${SERVICES[$service]}" "${SERVICE_PORTS[$service]}"
|
||||
done
|
||||
|
||||
echo ""
|
||||
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}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 "Usage: $0 <command> [service|group] [options]"
|
||||
echo ""
|
||||
echo "Commands:"
|
||||
echo " start <service|group> Start service or service group"
|
||||
echo " stop <service|group> Stop service or service group"
|
||||
echo " restart <service|group> Restart service or service group"
|
||||
echo " remove <service|group> Remove service container"
|
||||
echo " logs <service> [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 "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 "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 "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
|
||||
db)
|
||||
echo "$GROUP_DB"
|
||||
;;
|
||||
cache)
|
||||
echo "$GROUP_CACHE"
|
||||
;;
|
||||
registry)
|
||||
echo "$GROUP_REGISTRY"
|
||||
;;
|
||||
all)
|
||||
echo "$GROUP_ALL"
|
||||
;;
|
||||
*)
|
||||
echo "$input"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Check if it's a compose service
|
||||
is_compose_service() {
|
||||
local service=$1
|
||||
case $service in
|
||||
apollo|nacos|redis-cluster|consul)
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Pull images
|
||||
pull_images() {
|
||||
local services=$1
|
||||
|
||||
if [ -z "$services" ]; then
|
||||
services="$GROUP_ALL"
|
||||
fi
|
||||
|
||||
for service in $services; do
|
||||
if [ -n "${SERVICES[$service]}" ]; then
|
||||
print_info "Pulling image: ${SERVICES[$service]}"
|
||||
docker pull "${SERVICES[$service]}"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# Clean all goframe containers
|
||||
clean_all() {
|
||||
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 "Removed: $container"
|
||||
done
|
||||
else
|
||||
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")
|
||||
|
||||
if docker ps --format '{{.Names}}' 2>/dev/null | grep -q "^${container_name}$"; then
|
||||
echo -e "${GREEN}*${NC}"
|
||||
else
|
||||
echo " "
|
||||
fi
|
||||
}
|
||||
|
||||
# Get compose service status mark
|
||||
get_compose_status_mark() {
|
||||
local service=$1
|
||||
local running=0
|
||||
|
||||
case $service in
|
||||
apollo)
|
||||
running=$(docker ps --filter "name=apollo" --format '{{.Names}}' 2>/dev/null | wc -l)
|
||||
;;
|
||||
nacos)
|
||||
running=$(docker ps --filter "name=nacos" --format '{{.Names}}' 2>/dev/null | wc -l)
|
||||
;;
|
||||
redis-cluster)
|
||||
running=$(docker ps --filter "name=redis-" --format '{{.Names}}' 2>/dev/null | wc -l)
|
||||
;;
|
||||
consul)
|
||||
running=$(docker ps --filter "name=consul" --format '{{.Names}}' 2>/dev/null | wc -l)
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ "$running" -gt 0 ]; then
|
||||
echo -e "${GREEN}*${NC}"
|
||||
else
|
||||
echo " "
|
||||
fi
|
||||
}
|
||||
|
||||
# Service selection menu
|
||||
select_service_menu() {
|
||||
local action=$1
|
||||
local action_name=$2
|
||||
|
||||
echo ""
|
||||
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} indicates running)"
|
||||
fi
|
||||
echo ""
|
||||
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" \
|
||||
"$(get_service_status_mark mariadb)" "$(get_service_status_mark postgres)" "$(get_service_status_mark mssql)"
|
||||
printf " %b7) clickhouse %b8) polaris %b9) oracle\n" \
|
||||
"$(get_service_status_mark clickhouse)" "$(get_service_status_mark polaris)" "$(get_service_status_mark oracle)"
|
||||
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 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}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) Back to main menu"
|
||||
echo ""
|
||||
read -p "Select [0-20]: " svc_choice
|
||||
|
||||
local svc=""
|
||||
case $svc_choice in
|
||||
1) svc="etcd" ;;
|
||||
2) svc="redis" ;;
|
||||
3) svc="mysql" ;;
|
||||
4) svc="mariadb" ;;
|
||||
5) svc="postgres" ;;
|
||||
6) svc="mssql" ;;
|
||||
7) svc="clickhouse" ;;
|
||||
8) svc="polaris" ;;
|
||||
9) svc="oracle" ;;
|
||||
10) svc="dm" ;;
|
||||
11) svc="gaussdb" ;;
|
||||
12) svc="zookeeper" ;;
|
||||
13) svc="apollo" ;;
|
||||
14) svc="nacos" ;;
|
||||
15) svc="redis-cluster" ;;
|
||||
16) svc="consul" ;;
|
||||
17) svc="db" ;;
|
||||
18) svc="cache" ;;
|
||||
19) svc="registry" ;;
|
||||
20) svc="all" ;;
|
||||
0) return ;;
|
||||
*)
|
||||
print_error "Invalid selection"
|
||||
return
|
||||
;;
|
||||
esac
|
||||
|
||||
case $action in
|
||||
start)
|
||||
if is_compose_service "$svc"; then
|
||||
start_compose_service "$svc"
|
||||
else
|
||||
for s in $(parse_services "$svc"); do
|
||||
start_service "$s"
|
||||
done
|
||||
fi
|
||||
;;
|
||||
stop)
|
||||
if is_compose_service "$svc"; then
|
||||
stop_compose_service "$svc"
|
||||
else
|
||||
for s in $(parse_services "$svc"); do
|
||||
stop_service "$s"
|
||||
done
|
||||
fi
|
||||
;;
|
||||
restart)
|
||||
if is_compose_service "$svc"; then
|
||||
stop_compose_service "$svc"
|
||||
start_compose_service "$svc"
|
||||
else
|
||||
for s in $(parse_services "$svc"); do
|
||||
stop_service "$s"
|
||||
start_service "$s"
|
||||
done
|
||||
fi
|
||||
;;
|
||||
remove)
|
||||
for s in $(parse_services "$svc"); do
|
||||
remove_service "$s"
|
||||
done
|
||||
;;
|
||||
logs)
|
||||
if is_compose_service "$svc"; then
|
||||
print_error "For Compose services, please use 'docker compose logs'"
|
||||
else
|
||||
read -p "Number of lines (default 100): " lines
|
||||
lines=${lines:-100}
|
||||
logs_service "$svc" "$lines"
|
||||
fi
|
||||
;;
|
||||
pull)
|
||||
pull_images "$(parse_services "$svc")"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Interactive menu
|
||||
interactive_menu() {
|
||||
while true; do
|
||||
echo ""
|
||||
echo -e "${CYAN}========== GoFrame Docker Services Manager ==========${NC}"
|
||||
echo ""
|
||||
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 "Select operation [0-9]: " choice
|
||||
|
||||
case $choice in
|
||||
1)
|
||||
select_service_menu "start" "Start"
|
||||
;;
|
||||
2)
|
||||
select_service_menu "stop" "Stop"
|
||||
;;
|
||||
3)
|
||||
select_service_menu "restart" "Restart"
|
||||
;;
|
||||
4)
|
||||
select_service_menu "remove" "Remove"
|
||||
;;
|
||||
5)
|
||||
select_service_menu "logs" "View Logs"
|
||||
;;
|
||||
6)
|
||||
show_status
|
||||
;;
|
||||
7)
|
||||
show_service_info
|
||||
;;
|
||||
8)
|
||||
read -p "Confirm removing all goframe containers? [y/N]: " confirm
|
||||
if [[ "$confirm" =~ ^[Yy]$ ]]; then
|
||||
clean_all
|
||||
fi
|
||||
;;
|
||||
9)
|
||||
select_service_menu "pull" "Pull Images"
|
||||
;;
|
||||
0)
|
||||
echo "Goodbye!"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
print_error "Invalid selection"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
# Main function
|
||||
main() {
|
||||
check_docker
|
||||
|
||||
if [ $# -eq 0 ]; then
|
||||
interactive_menu
|
||||
exit 0
|
||||
fi
|
||||
|
||||
local command=$1
|
||||
local target=$2
|
||||
local extra=$3
|
||||
|
||||
case $command in
|
||||
start)
|
||||
if [ -z "$target" ]; then
|
||||
print_error "Please specify service name or service group"
|
||||
exit 1
|
||||
fi
|
||||
if is_compose_service "$target"; then
|
||||
start_compose_service "$target"
|
||||
else
|
||||
for service in $(parse_services "$target"); do
|
||||
start_service "$service"
|
||||
done
|
||||
fi
|
||||
;;
|
||||
stop)
|
||||
if [ -z "$target" ]; then
|
||||
print_error "Please specify service name or service group"
|
||||
exit 1
|
||||
fi
|
||||
if is_compose_service "$target"; then
|
||||
stop_compose_service "$target"
|
||||
else
|
||||
for service in $(parse_services "$target"); do
|
||||
stop_service "$service"
|
||||
done
|
||||
fi
|
||||
;;
|
||||
restart)
|
||||
if [ -z "$target" ]; then
|
||||
print_error "Please specify service name or service group"
|
||||
exit 1
|
||||
fi
|
||||
if is_compose_service "$target"; then
|
||||
stop_compose_service "$target"
|
||||
start_compose_service "$target"
|
||||
else
|
||||
for service in $(parse_services "$target"); do
|
||||
stop_service "$service"
|
||||
start_service "$service"
|
||||
done
|
||||
fi
|
||||
;;
|
||||
remove|rm)
|
||||
if [ -z "$target" ]; then
|
||||
print_error "Please specify service name or service group"
|
||||
exit 1
|
||||
fi
|
||||
for service in $(parse_services "$target"); do
|
||||
remove_service "$service"
|
||||
done
|
||||
;;
|
||||
logs)
|
||||
if [ -z "$target" ]; then
|
||||
print_error "Please specify service name"
|
||||
exit 1
|
||||
fi
|
||||
logs_service "$target" "${extra:-100}"
|
||||
;;
|
||||
status|ps)
|
||||
show_status
|
||||
;;
|
||||
info|list)
|
||||
show_service_info
|
||||
;;
|
||||
clean)
|
||||
clean_all
|
||||
;;
|
||||
pull)
|
||||
pull_images "$target"
|
||||
;;
|
||||
help|--help|-h)
|
||||
show_help
|
||||
;;
|
||||
*)
|
||||
print_error "Unknown command: $command"
|
||||
show_help
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
main "$@"
|
||||
10
Makefile
10
Makefile
@ -76,3 +76,13 @@ subsync: subup
|
||||
git push origin; \
|
||||
fi; \
|
||||
cd ..;
|
||||
|
||||
# manage docker services for local development
|
||||
# usage: make docker or make docker cmd=start svc=mysql
|
||||
.PHONY: docker
|
||||
docker:
|
||||
@if [ -z "$(cmd)" ]; then \
|
||||
./.github/workflows/scripts/docker-services.sh; \
|
||||
else \
|
||||
./.github/workflows/scripts/docker-services.sh $(cmd) $(svc) $(extra); \
|
||||
fi
|
||||
|
||||
@ -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) 💖
|
||||
|
||||
<a href="https://github.com/gogf/gf/graphs/contributors">
|
||||
<img src="https://goframe.org/img/contributors.svg?version=v2.9.7" alt="goframe contributors"/>
|
||||
<img src="https://goframe.org/img/contributors.svg?version=v2.9.8" alt="goframe contributors"/>
|
||||
</a>
|
||||
|
||||
## License
|
||||
|
||||
@ -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
|
||||
|
||||
@ -50,8 +50,9 @@ gf init my-mono-repo -a
|
||||
gf init my-project -u
|
||||
gf init my-project -g "github.com/myorg/myproject"
|
||||
gf init -r github.com/gogf/template-single my-project
|
||||
gf init -r github.com/gogf/template-single my-project -s
|
||||
gf init -r github.com/gogf/examples/httpserver/jwt my-jwt
|
||||
gf init -r github.com/gogf/gf/cmd/gf/v2@v2.9.7 mygf
|
||||
gf init -r github.com/gogf/gf/cmd/gf/v2 mygf -s
|
||||
gf init -i
|
||||
`
|
||||
cInitNameBrief = `
|
||||
@ -237,6 +238,9 @@ func (c cInit) initFromBuiltin(ctx context.Context, in cInitInput) (out *cInitOu
|
||||
return
|
||||
}
|
||||
|
||||
// Format the generated Go files.
|
||||
utils.GoFmt(in.Name)
|
||||
|
||||
// Update the GoFrame version.
|
||||
if in.Update {
|
||||
mlog.Print("update goframe...")
|
||||
|
||||
@ -22,7 +22,7 @@ func Test_Gen_Ctrl_Default(t *testing.T) {
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
var (
|
||||
path = gfile.Temp(guid.S())
|
||||
apiFolder = gtest.DataPath("genctrl", "api")
|
||||
apiFolder = gtest.DataPath("genctrl", "default", "api")
|
||||
in = genctrl.CGenCtrlInput{
|
||||
SrcFolder: apiFolder,
|
||||
DstFolder: path,
|
||||
@ -39,7 +39,7 @@ func Test_Gen_Ctrl_Default(t *testing.T) {
|
||||
|
||||
err = gfile.Mkdir(path)
|
||||
t.AssertNil(err)
|
||||
defer gfile.Remove(path)
|
||||
defer gfile.RemoveAll(path)
|
||||
|
||||
_, err = genctrl.CGenCtrl{}.Ctrl(ctx, in)
|
||||
t.AssertNil(err)
|
||||
@ -49,7 +49,7 @@ func Test_Gen_Ctrl_Default(t *testing.T) {
|
||||
genApi = apiFolder + filepath.FromSlash("/article/article.go")
|
||||
genApiExpect = apiFolder + filepath.FromSlash("/article/article_expect.go")
|
||||
)
|
||||
defer gfile.Remove(genApi)
|
||||
defer gfile.RemoveAll(genApi)
|
||||
t.Assert(gfile.GetContents(genApi), gfile.GetContents(genApiExpect))
|
||||
|
||||
// files
|
||||
@ -67,7 +67,7 @@ func Test_Gen_Ctrl_Default(t *testing.T) {
|
||||
})
|
||||
|
||||
// content
|
||||
testPath := gtest.DataPath("genctrl", "controller")
|
||||
testPath := gtest.DataPath("genctrl", "default", "controller")
|
||||
expectFiles := []string{
|
||||
testPath + filepath.FromSlash("/article/article.go"),
|
||||
testPath + filepath.FromSlash("/article/article_new.go"),
|
||||
@ -84,6 +84,104 @@ func Test_Gen_Ctrl_Default(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func Test_Gen_Ctrl_Default_Multi(t *testing.T) {
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
var (
|
||||
path = gfile.Temp(guid.S())
|
||||
apiFolder = gtest.DataPath("genctrl", "multi", "api")
|
||||
in = genctrl.CGenCtrlInput{
|
||||
SrcFolder: apiFolder,
|
||||
DstFolder: path,
|
||||
WatchFile: "",
|
||||
SdkPath: "",
|
||||
SdkStdVersion: false,
|
||||
SdkNoV1: false,
|
||||
Clear: false,
|
||||
Merge: false,
|
||||
}
|
||||
)
|
||||
|
||||
err := gutil.FillStructWithDefault(&in)
|
||||
t.AssertNil(err)
|
||||
|
||||
err = gfile.Mkdir(path)
|
||||
t.AssertNil(err)
|
||||
defer gfile.RemoveAll(path)
|
||||
|
||||
_, err = genctrl.CGenCtrl{}.Ctrl(ctx, in)
|
||||
t.AssertNil(err)
|
||||
|
||||
// apiInterface file
|
||||
var (
|
||||
genApiSlice = []string{
|
||||
apiFolder + filepath.FromSlash("/admin/article/article.go"),
|
||||
apiFolder + filepath.FromSlash("/admin/user/user.go"),
|
||||
apiFolder + filepath.FromSlash("/app/user/user.go"),
|
||||
apiFolder + filepath.FromSlash("/app/user/user_ext/user_ext.go"),
|
||||
}
|
||||
genApiSliceExpect = []string{
|
||||
apiFolder + filepath.FromSlash("/admin/article/article_expect.go"),
|
||||
apiFolder + filepath.FromSlash("/admin/user/user_expect.go"),
|
||||
apiFolder + filepath.FromSlash("/app/user/user_expect.go"),
|
||||
apiFolder + filepath.FromSlash("/app/user/user_ext/user_ext_expect.go"),
|
||||
}
|
||||
)
|
||||
|
||||
for i := range genApiSlice {
|
||||
t.Assert(gfile.GetContents(genApiSlice[i]), gfile.GetContents(genApiSliceExpect[i]))
|
||||
gfile.RemoveAll(genApiSlice[i])
|
||||
}
|
||||
|
||||
// files
|
||||
files, err := gfile.ScanDir(path, "*.go", true)
|
||||
t.AssertNil(err)
|
||||
t.Assert(files, []string{
|
||||
path + filepath.FromSlash("/admin/article/article.go"),
|
||||
path + filepath.FromSlash("/admin/article/article_new.go"),
|
||||
path + filepath.FromSlash("/admin/article/article_v1_create.go"),
|
||||
|
||||
path + filepath.FromSlash("/admin/user/user.go"),
|
||||
path + filepath.FromSlash("/admin/user/user_new.go"),
|
||||
path + filepath.FromSlash("/admin/user/user_v1_create.go"),
|
||||
|
||||
path + filepath.FromSlash("/app/user/user.go"),
|
||||
path + filepath.FromSlash("/app/user/user_ext/user_ext.go"),
|
||||
path + filepath.FromSlash("/app/user/user_ext/user_ext_new.go"),
|
||||
path + filepath.FromSlash("/app/user/user_ext/user_ext_v1_create.go"),
|
||||
path + filepath.FromSlash("/app/user/user_ext/user_ext_v1_update.go"),
|
||||
|
||||
path + filepath.FromSlash("/app/user/user_new.go"),
|
||||
path + filepath.FromSlash("/app/user/user_v1_create.go"),
|
||||
path + filepath.FromSlash("/app/user/user_v1_update.go"),
|
||||
})
|
||||
|
||||
// content
|
||||
testPath := gtest.DataPath("genctrl", "multi", "controller")
|
||||
expectFiles := []string{
|
||||
testPath + filepath.FromSlash("/admin/article/article.go"),
|
||||
testPath + filepath.FromSlash("/admin/article/article_new.go"),
|
||||
testPath + filepath.FromSlash("/admin/article/article_v1_create.go"),
|
||||
|
||||
testPath + filepath.FromSlash("/admin/user/user.go"),
|
||||
testPath + filepath.FromSlash("/admin/user/user_new.go"),
|
||||
testPath + filepath.FromSlash("/admin/user/user_v1_create.go"),
|
||||
|
||||
testPath + filepath.FromSlash("/app/user/user.go"),
|
||||
testPath + filepath.FromSlash("/app/user/user_ext/user_ext.go"),
|
||||
testPath + filepath.FromSlash("/app/user/user_ext/user_ext_new.go"),
|
||||
testPath + filepath.FromSlash("/app/user/user_ext/user_ext_v1_create.go"),
|
||||
testPath + filepath.FromSlash("/app/user/user_ext/user_ext_v1_update.go"),
|
||||
|
||||
testPath + filepath.FromSlash("/app/user/user_new.go"),
|
||||
testPath + filepath.FromSlash("/app/user/user_v1_create.go"),
|
||||
testPath + filepath.FromSlash("/app/user/user_v1_update.go"),
|
||||
}
|
||||
for i := range files {
|
||||
t.Assert(gfile.GetContents(files[i]), gfile.GetContents(expectFiles[i]))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func expectFilesContent(t *gtest.T, paths []string, expectPaths []string) {
|
||||
for i, expectFile := range expectPaths {
|
||||
val := gfile.GetContents(paths[i])
|
||||
@ -98,8 +196,8 @@ func Test_Gen_Ctrl_UseMerge_AddNewFile(t *testing.T) {
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
var (
|
||||
ctrlPath = gfile.Temp(guid.S())
|
||||
//ctrlPath = gtest.DataPath("issue", "3460", "controller")
|
||||
apiFolder = gtest.DataPath("genctrl-merge", "add_new_file", "api")
|
||||
// ctrlPath = gtest.DataPath("issue", "3460", "controller")
|
||||
apiFolder = gtest.DataPath("genctrl", "merge", "add_new_file", "api")
|
||||
in = genctrl.CGenCtrlInput{
|
||||
SrcFolder: apiFolder,
|
||||
DstFolder: ctrlPath,
|
||||
@ -118,7 +216,7 @@ type DictTypeAddRes struct {
|
||||
|
||||
err := gfile.Mkdir(ctrlPath)
|
||||
t.AssertNil(err)
|
||||
defer gfile.Remove(ctrlPath)
|
||||
defer gfile.RemoveAll(ctrlPath)
|
||||
|
||||
_, err = genctrl.CGenCtrl{}.Ctrl(ctx, in)
|
||||
t.AssertNil(err)
|
||||
@ -127,7 +225,7 @@ type DictTypeAddRes struct {
|
||||
genApi = filepath.Join(apiFolder, "/dict/dict.go")
|
||||
genApiExpect = filepath.Join(apiFolder, "/dict/dict_expect.go")
|
||||
)
|
||||
defer gfile.Remove(genApi)
|
||||
defer gfile.RemoveAll(genApi)
|
||||
t.Assert(gfile.GetContents(genApi), gfile.GetContents(genApiExpect))
|
||||
|
||||
genCtrlFiles, err := gfile.ScanDir(ctrlPath, "*.go", true)
|
||||
@ -138,7 +236,7 @@ type DictTypeAddRes struct {
|
||||
filepath.Join(ctrlPath, "/dict/dict_v1_dict_type.go"),
|
||||
})
|
||||
|
||||
expectCtrlPath := gtest.DataPath("genctrl-merge", "add_new_file", "controller")
|
||||
expectCtrlPath := gtest.DataPath("genctrl", "merge", "add_new_file", "controller")
|
||||
expectFiles := []string{
|
||||
filepath.Join(expectCtrlPath, "/dict/dict.go"),
|
||||
filepath.Join(expectCtrlPath, "/dict/dict_new.go"),
|
||||
@ -152,7 +250,7 @@ type DictTypeAddRes struct {
|
||||
newApiFilePath := filepath.Join(apiFolder, "/dict/v1/test_new.go")
|
||||
err = gfile.PutContents(newApiFilePath, testNewApiFile)
|
||||
t.AssertNil(err)
|
||||
defer gfile.Remove(newApiFilePath)
|
||||
defer gfile.RemoveAll(newApiFilePath)
|
||||
|
||||
// Then execute the command
|
||||
_, err = genctrl.CGenCtrl{}.Ctrl(ctx, in)
|
||||
@ -179,8 +277,8 @@ func Test_Gen_Ctrl_UseMerge_AddNewCtrl(t *testing.T) {
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
var (
|
||||
ctrlPath = gfile.Temp(guid.S())
|
||||
//ctrlPath = gtest.DataPath("issue", "3460", "controller")
|
||||
apiFolder = gtest.DataPath("genctrl-merge", "add_new_ctrl", "api")
|
||||
// ctrlPath = gtest.DataPath("issue", "3460", "controller")
|
||||
apiFolder = gtest.DataPath("genctrl", "merge", "add_new_ctrl", "api")
|
||||
in = genctrl.CGenCtrlInput{
|
||||
SrcFolder: apiFolder,
|
||||
DstFolder: ctrlPath,
|
||||
@ -190,7 +288,7 @@ func Test_Gen_Ctrl_UseMerge_AddNewCtrl(t *testing.T) {
|
||||
|
||||
err := gfile.Mkdir(ctrlPath)
|
||||
t.AssertNil(err)
|
||||
defer gfile.Remove(ctrlPath)
|
||||
defer gfile.RemoveAll(ctrlPath)
|
||||
|
||||
_, err = genctrl.CGenCtrl{}.Ctrl(ctx, in)
|
||||
t.AssertNil(err)
|
||||
@ -199,7 +297,7 @@ func Test_Gen_Ctrl_UseMerge_AddNewCtrl(t *testing.T) {
|
||||
genApi = filepath.Join(apiFolder, "/dict/dict.go")
|
||||
genApiExpect = filepath.Join(apiFolder, "/dict/dict_expect.go")
|
||||
)
|
||||
defer gfile.Remove(genApi)
|
||||
defer gfile.RemoveAll(genApi)
|
||||
t.Assert(gfile.GetContents(genApi), gfile.GetContents(genApiExpect))
|
||||
|
||||
genCtrlFiles, err := gfile.ScanDir(ctrlPath, "*.go", true)
|
||||
@ -210,7 +308,7 @@ func Test_Gen_Ctrl_UseMerge_AddNewCtrl(t *testing.T) {
|
||||
filepath.Join(ctrlPath, "/dict/dict_v1_dict_type.go"),
|
||||
})
|
||||
|
||||
expectCtrlPath := gtest.DataPath("genctrl-merge", "add_new_ctrl", "controller")
|
||||
expectCtrlPath := gtest.DataPath("genctrl", "merge", "add_new_ctrl", "controller")
|
||||
expectFiles := []string{
|
||||
filepath.Join(expectCtrlPath, "/dict/dict.go"),
|
||||
filepath.Join(expectCtrlPath, "/dict/dict_new.go"),
|
||||
@ -236,7 +334,7 @@ type DictTypeAddRes struct {
|
||||
err = gfile.PutContentsAppend(dictModuleFileName, testNewApiFile)
|
||||
t.AssertNil(err)
|
||||
|
||||
//==================================
|
||||
// ==================================
|
||||
// Then execute the command
|
||||
_, err = genctrl.CGenCtrl{}.Ctrl(ctx, in)
|
||||
t.AssertNil(err)
|
||||
@ -262,7 +360,7 @@ func Test_Gen_Ctrl_UseMerge_Issue3460(t *testing.T) {
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
var (
|
||||
ctrlPath = gfile.Temp(guid.S())
|
||||
//ctrlPath = gtest.DataPath("issue", "3460", "controller")
|
||||
// ctrlPath = gtest.DataPath("issue", "3460", "controller")
|
||||
apiFolder = gtest.DataPath("issue", "3460", "api")
|
||||
in = genctrl.CGenCtrlInput{
|
||||
SrcFolder: apiFolder,
|
||||
@ -278,7 +376,7 @@ func Test_Gen_Ctrl_UseMerge_Issue3460(t *testing.T) {
|
||||
|
||||
err := gfile.Mkdir(ctrlPath)
|
||||
t.AssertNil(err)
|
||||
defer gfile.Remove(ctrlPath)
|
||||
defer gfile.RemoveAll(ctrlPath)
|
||||
|
||||
_, err = genctrl.CGenCtrl{}.Ctrl(ctx, in)
|
||||
t.AssertNil(err)
|
||||
|
||||
@ -89,28 +89,11 @@ func (c CGenCtrl) Ctrl(ctx context.Context, in CGenCtrlInput) (out *CGenCtrlOutp
|
||||
if !gfile.Exists(in.SrcFolder) {
|
||||
mlog.Fatalf(`source folder path "%s" does not exist`, in.SrcFolder)
|
||||
}
|
||||
// retrieve all api modules.
|
||||
apiModuleFolderPaths, err := gfile.ScanDir(in.SrcFolder, "*", false)
|
||||
|
||||
err = c.generateByModules(in)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, apiModuleFolderPath := range apiModuleFolderPaths {
|
||||
if !gfile.IsDir(apiModuleFolderPath) {
|
||||
continue
|
||||
}
|
||||
// generate go files by api module.
|
||||
var (
|
||||
module = gfile.Basename(apiModuleFolderPath)
|
||||
dstModuleFolderPath = gfile.Join(in.DstFolder, module)
|
||||
)
|
||||
err = c.generateByModule(
|
||||
apiModuleFolderPath, dstModuleFolderPath, in.SdkPath,
|
||||
in.SdkStdVersion, in.SdkNoV1, in.Clear, in.Merge,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
mlog.Print(`done!`)
|
||||
return
|
||||
@ -163,6 +146,56 @@ func (c CGenCtrl) generateByWatchFile(watchFile, sdkPath string, sdkStdVersion,
|
||||
)
|
||||
}
|
||||
|
||||
// generateByModules recursively calls generateByModule for multi-level modules generation.
|
||||
func (c CGenCtrl) generateByModules(in CGenCtrlInput) (err error) {
|
||||
// read root folder, example: api/user or api/app
|
||||
moduleFolderPaths, err := gfile.ScanDir(in.SrcFolder, "*", false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, moduleFolder := range moduleFolderPaths {
|
||||
if !gfile.IsDir(moduleFolder) {
|
||||
continue
|
||||
}
|
||||
|
||||
// read children folder, example: api/user/v1 or api/app/user
|
||||
childrenFolderPaths, err := gfile.ScanDir(moduleFolder, "*", false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, childrenFolderPath := range childrenFolderPaths {
|
||||
if !gfile.IsDir(childrenFolderPath) {
|
||||
continue
|
||||
}
|
||||
|
||||
var (
|
||||
inCopy = in
|
||||
module = gfile.Basename(moduleFolder)
|
||||
)
|
||||
inCopy.SrcFolder = gfile.Join(in.SrcFolder, module)
|
||||
inCopy.DstFolder = gfile.Join(in.DstFolder, module)
|
||||
err = c.generateByModules(inCopy)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// generate go files by api module.
|
||||
var (
|
||||
module = gfile.Basename(moduleFolder)
|
||||
dstModuleFolderPath = gfile.Join(in.DstFolder, module)
|
||||
)
|
||||
err = c.generateByModule(
|
||||
moduleFolder, dstModuleFolderPath, in.SdkPath,
|
||||
in.SdkStdVersion, in.SdkNoV1, in.Clear, in.Merge,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// parseApiModule parses certain api and generate associated go files by certain module, not all api modules.
|
||||
func (c CGenCtrl) generateByModule(
|
||||
apiModuleFolderPath, dstModuleFolderPath, sdkPath string,
|
||||
|
||||
@ -8,6 +8,9 @@ package genctrl
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
@ -144,8 +147,8 @@ func (c *controllerGenerator) doGenerateCtrlItem(dstModuleFolderPath string, ite
|
||||
"{MethodName}": item.MethodName,
|
||||
"{MethodComment}": item.GetComment(),
|
||||
})
|
||||
|
||||
if gstr.Contains(gfile.GetContents(methodFilePath), fmt.Sprintf(`func (c *%v) %v(`, ctrlName, item.MethodName)) {
|
||||
// Use AST-based checking for more accurate method detection
|
||||
if methodExists(methodFilePath, ctrlName, item.MethodName) {
|
||||
return
|
||||
}
|
||||
if err = gfile.PutContentsAppend(methodFilePath, gstr.TrimLeft(content)); err != nil {
|
||||
@ -170,7 +173,6 @@ func (c *controllerGenerator) doGenerateCtrlItem(dstModuleFolderPath string, ite
|
||||
|
||||
// use -merge
|
||||
func (c *controllerGenerator) doGenerateCtrlMergeItem(dstModuleFolderPath string, apiItems []apiItem, doneApiSet *gset.StrSet) (err error) {
|
||||
|
||||
type controllerFileItem struct {
|
||||
module string
|
||||
version string
|
||||
@ -193,13 +195,23 @@ func (c *controllerGenerator) doGenerateCtrlMergeItem(dstModuleFolderPath string
|
||||
ctrlFileItemMap[api.FileName] = ctrlFileItem
|
||||
}
|
||||
|
||||
ctrlName := fmt.Sprintf(`Controller%s`, gstr.UcFirst(api.Version))
|
||||
ctrl := gstr.TrimLeft(gstr.ReplaceByMap(consts.TemplateGenCtrlControllerMethodFuncMerge, g.MapStrStr{
|
||||
"{Module}": api.Module,
|
||||
"{CtrlName}": fmt.Sprintf(`Controller%s`, gstr.UcFirst(api.Version)),
|
||||
"{CtrlName}": ctrlName,
|
||||
"{Version}": api.Version,
|
||||
"{MethodName}": api.MethodName,
|
||||
"{MethodComment}": api.GetComment(),
|
||||
}))
|
||||
|
||||
ctrlFilePath := gfile.Join(dstModuleFolderPath, fmt.Sprintf(
|
||||
`%s_%s_%s.go`, ctrlFileItem.module, ctrlFileItem.version, api.FileName,
|
||||
))
|
||||
// Use AST-based checking for more accurate method detection
|
||||
if methodExists(ctrlFilePath, ctrlName, api.MethodName) {
|
||||
return
|
||||
}
|
||||
|
||||
ctrlFileItem.controllers.WriteString(ctrl)
|
||||
doneApiSet.Add(api.String())
|
||||
}
|
||||
@ -229,3 +241,41 @@ func (c *controllerGenerator) doGenerateCtrlMergeItem(dstModuleFolderPath string
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// methodExists checks if a method with the given receiver type and name exists in the file.
|
||||
// It uses AST parsing to accurately detect method definitions regardless of formatting.
|
||||
// This handles various code formatting styles including multi-line method signatures.
|
||||
func methodExists(filePath, ctrlName, methodName string) bool {
|
||||
fset := token.NewFileSet()
|
||||
node, err := parser.ParseFile(fset, filePath, nil, parser.ParseComments)
|
||||
if err != nil {
|
||||
// If parsing fails (e.g., file doesn't exist or invalid syntax), return false
|
||||
return false
|
||||
}
|
||||
for _, decl := range node.Decls {
|
||||
funcDecl, ok := decl.(*ast.FuncDecl)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
// Check if it's a method (has receiver)
|
||||
if funcDecl.Recv != nil && len(funcDecl.Recv.List) > 0 {
|
||||
// Extract receiver type name
|
||||
// Handle both *T and T patterns
|
||||
recvType := ""
|
||||
switch t := funcDecl.Recv.List[0].Type.(type) {
|
||||
case *ast.StarExpr:
|
||||
if ident, ok := t.X.(*ast.Ident); ok {
|
||||
recvType = ident.Name
|
||||
}
|
||||
case *ast.Ident:
|
||||
recvType = t.Name
|
||||
}
|
||||
|
||||
// Check if both receiver type and method name match
|
||||
if recvType == ctrlName && funcDecl.Name.Name == methodName {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@ -86,7 +86,7 @@ func processGoModule(ctx context.Context, repo, name string, opts *ProcessOption
|
||||
// 1. Determine version to use
|
||||
var targetVersion string
|
||||
if specifiedVersion != "" {
|
||||
// User specified version
|
||||
// User specified version, try to use it first
|
||||
targetVersion = specifiedVersion
|
||||
mlog.Printf("Using specified version: %s", targetVersion)
|
||||
} else if opts.SelectVersion {
|
||||
@ -120,8 +120,41 @@ func processGoModule(ctx context.Context, repo, name string, opts *ProcessOption
|
||||
repoWithVersion := modulePath + "@" + targetVersion
|
||||
srcDir, err := downloadTemplate(ctx, repoWithVersion)
|
||||
if err != nil {
|
||||
mlog.Printf("Download failed: %v", err)
|
||||
return err
|
||||
// If specified version download failed, offer to select from available versions
|
||||
if specifiedVersion != "" {
|
||||
mlog.Printf("Failed to download specified version '%s': %v", specifiedVersion, err)
|
||||
mlog.Print("Fetching available versions...")
|
||||
|
||||
versionInfo, verErr := GetModuleVersions(ctx, modulePath)
|
||||
if verErr != nil {
|
||||
mlog.Printf("Failed to get available versions: %v", verErr)
|
||||
return err // Return original download error
|
||||
}
|
||||
|
||||
if len(versionInfo.Versions) == 0 {
|
||||
mlog.Print("No versions available for this module")
|
||||
return err
|
||||
}
|
||||
|
||||
// Let user select from available versions
|
||||
selectedVersion, selErr := SelectVersion(ctx, versionInfo.Versions, modulePath)
|
||||
if selErr != nil {
|
||||
mlog.Printf("Version selection failed: %v", selErr)
|
||||
return selErr
|
||||
}
|
||||
|
||||
// Retry download with selected version
|
||||
targetVersion = selectedVersion
|
||||
repoWithVersion = modulePath + "@" + targetVersion
|
||||
srcDir, err = downloadTemplate(ctx, repoWithVersion)
|
||||
if err != nil {
|
||||
mlog.Printf("Download failed: %v", err)
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
mlog.Printf("Download failed: %v", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
mlog.Debugf("Template located at: %s", srcDir)
|
||||
|
||||
@ -78,11 +78,10 @@ func (r *ASTReplacer) ReplaceInFile(ctx context.Context, filePath string) error
|
||||
return nil
|
||||
}
|
||||
|
||||
// Write back to file
|
||||
// Write back to file without formatting.
|
||||
// Formatting will be handled by formatGoFiles after all replacements are done.
|
||||
var buf bytes.Buffer
|
||||
// Use default printer configuration to match gofmt output
|
||||
cfg := &printer.Config{}
|
||||
if err := cfg.Fprint(&buf, r.fset, file); err != nil {
|
||||
if err := printer.Fprint(&buf, r.fset, file); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@ -9,6 +9,7 @@ package geninit
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"go/format"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/gogf/gf/v2/os/gfile"
|
||||
@ -81,6 +82,11 @@ func generateProject(ctx context.Context, srcPath, name, oldModule, newModule st
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
@ -108,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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,7 +4,7 @@ go 1.23.0
|
||||
|
||||
toolchain go1.24.6
|
||||
|
||||
require github.com/gogf/gf/v2 v2.9.6
|
||||
require github.com/gogf/gf/v2 v2.9.7
|
||||
|
||||
require (
|
||||
go.opentelemetry.io/otel v1.38.0 // indirect
|
||||
|
||||
@ -7,8 +7,8 @@ package article
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/gogf/gf/cmd/gf/v2/internal/cmd/testdata/genctrl/api/article/v1"
|
||||
"github.com/gogf/gf/cmd/gf/v2/internal/cmd/testdata/genctrl/api/article/v2"
|
||||
"github.com/gogf/gf/cmd/gf/v2/internal/cmd/testdata/genctrl/default/api/article/v1"
|
||||
"github.com/gogf/gf/cmd/gf/v2/internal/cmd/testdata/genctrl/default/api/article/v2"
|
||||
)
|
||||
|
||||
type IArticleV1 interface {
|
||||
@ -5,7 +5,7 @@
|
||||
package article
|
||||
|
||||
import (
|
||||
"github.com/gogf/gf/cmd/gf/v2/internal/cmd/testdata/genctrl/api/article"
|
||||
"github.com/gogf/gf/cmd/gf/v2/internal/cmd/testdata/genctrl/default/api/article"
|
||||
)
|
||||
|
||||
type ControllerV1 struct{}
|
||||
@ -6,7 +6,7 @@ import (
|
||||
"github.com/gogf/gf/v2/errors/gcode"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
|
||||
"github.com/gogf/gf/cmd/gf/v2/internal/cmd/testdata/genctrl/api/article/v1"
|
||||
"github.com/gogf/gf/cmd/gf/v2/internal/cmd/testdata/genctrl/default/api/article/v1"
|
||||
)
|
||||
|
||||
// Create add title.
|
||||
@ -6,7 +6,7 @@ import (
|
||||
"github.com/gogf/gf/v2/errors/gcode"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
|
||||
"github.com/gogf/gf/cmd/gf/v2/internal/cmd/testdata/genctrl/api/article/v1"
|
||||
"github.com/gogf/gf/cmd/gf/v2/internal/cmd/testdata/genctrl/default/api/article/v1"
|
||||
)
|
||||
|
||||
func (c *ControllerV1) GetList(ctx context.Context, req *v1.GetListReq) (res *v1.GetListRes, err error) {
|
||||
@ -6,7 +6,7 @@ import (
|
||||
"github.com/gogf/gf/v2/errors/gcode"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
|
||||
"github.com/gogf/gf/cmd/gf/v2/internal/cmd/testdata/genctrl/api/article/v1"
|
||||
"github.com/gogf/gf/cmd/gf/v2/internal/cmd/testdata/genctrl/default/api/article/v1"
|
||||
)
|
||||
|
||||
func (c *ControllerV1) GetOne(ctx context.Context, req *v1.GetOneReq) (res *v1.GetOneRes, err error) {
|
||||
@ -6,7 +6,7 @@ import (
|
||||
"github.com/gogf/gf/v2/errors/gcode"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
|
||||
"github.com/gogf/gf/cmd/gf/v2/internal/cmd/testdata/genctrl/api/article/v1"
|
||||
"github.com/gogf/gf/cmd/gf/v2/internal/cmd/testdata/genctrl/default/api/article/v1"
|
||||
)
|
||||
|
||||
func (c *ControllerV1) Update(ctx context.Context, req *v1.UpdateReq) (res *v1.UpdateRes, err error) {
|
||||
@ -6,7 +6,7 @@ import (
|
||||
"github.com/gogf/gf/v2/errors/gcode"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
|
||||
"github.com/gogf/gf/cmd/gf/v2/internal/cmd/testdata/genctrl/api/article/v2"
|
||||
"github.com/gogf/gf/cmd/gf/v2/internal/cmd/testdata/genctrl/default/api/article/v2"
|
||||
)
|
||||
|
||||
func (c *ControllerV2) Create(ctx context.Context, req *v2.CreateReq) (res *v2.CreateRes, err error) {
|
||||
@ -6,7 +6,7 @@ import (
|
||||
"github.com/gogf/gf/v2/errors/gcode"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
|
||||
"github.com/gogf/gf/cmd/gf/v2/internal/cmd/testdata/genctrl/api/article/v2"
|
||||
"github.com/gogf/gf/cmd/gf/v2/internal/cmd/testdata/genctrl/default/api/article/v2"
|
||||
)
|
||||
|
||||
func (c *ControllerV2) Update(ctx context.Context, req *v2.UpdateReq) (res *v2.UpdateRes, err error) {
|
||||
@ -7,7 +7,7 @@ package dict
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/gogf/gf/cmd/gf/v2/internal/cmd/testdata/genctrl-merge/add_new_ctrl/api/dict/v1"
|
||||
"github.com/gogf/gf/cmd/gf/v2/internal/cmd/testdata/genctrl/merge/add_new_ctrl/api/dict/v1"
|
||||
)
|
||||
|
||||
type IDictV1 interface {
|
||||
@ -7,7 +7,7 @@ package dict
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/gogf/gf/cmd/gf/v2/internal/cmd/testdata/genctrl-merge/add_new_ctrl/api/dict/v1"
|
||||
"github.com/gogf/gf/cmd/gf/v2/internal/cmd/testdata/genctrl/merge/add_new_ctrl/api/dict/v1"
|
||||
)
|
||||
|
||||
type IDictV1 interface {
|
||||
@ -5,7 +5,7 @@
|
||||
package dict
|
||||
|
||||
import (
|
||||
"github.com/gogf/gf/cmd/gf/v2/internal/cmd/testdata/genctrl-merge/add_new_ctrl/api/dict"
|
||||
"github.com/gogf/gf/cmd/gf/v2/internal/cmd/testdata/genctrl/merge/add_new_ctrl/api/dict"
|
||||
)
|
||||
|
||||
type ControllerV1 struct{}
|
||||
@ -6,7 +6,7 @@ import (
|
||||
"github.com/gogf/gf/v2/errors/gcode"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
|
||||
"github.com/gogf/gf/cmd/gf/v2/internal/cmd/testdata/genctrl-merge/add_new_ctrl/api/dict/v1"
|
||||
"github.com/gogf/gf/cmd/gf/v2/internal/cmd/testdata/genctrl/merge/add_new_ctrl/api/dict/v1"
|
||||
)
|
||||
|
||||
func (c *ControllerV1) DictTypeAddPage(ctx context.Context, req *v1.DictTypeAddPageReq) (res *v1.DictTypeAddPageRes, err error) {
|
||||
@ -6,7 +6,7 @@ import (
|
||||
"github.com/gogf/gf/v2/errors/gcode"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
|
||||
"github.com/gogf/gf/cmd/gf/v2/internal/cmd/testdata/genctrl-merge/add_new_ctrl/api/dict/v1"
|
||||
"github.com/gogf/gf/cmd/gf/v2/internal/cmd/testdata/genctrl/merge/add_new_ctrl/api/dict/v1"
|
||||
)
|
||||
|
||||
func (c *ControllerV1) DictTypeAddPage(ctx context.Context, req *v1.DictTypeAddPageReq) (res *v1.DictTypeAddPageRes, err error) {
|
||||
@ -7,7 +7,7 @@ package dict
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/gogf/gf/cmd/gf/v2/internal/cmd/testdata/genctrl-merge/add_new_ctrl/api/dict/v1"
|
||||
"github.com/gogf/gf/cmd/gf/v2/internal/cmd/testdata/genctrl/merge/add_new_ctrl/api/dict/v1"
|
||||
)
|
||||
|
||||
type IDictV1 interface {
|
||||
@ -7,7 +7,7 @@ package dict
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/gogf/gf/cmd/gf/v2/internal/cmd/testdata/genctrl-merge/add_new_file/api/dict/v1"
|
||||
"github.com/gogf/gf/cmd/gf/v2/internal/cmd/testdata/genctrl/merge/add_new_file/api/dict/v1"
|
||||
)
|
||||
|
||||
type IDictV1 interface {
|
||||
@ -5,7 +5,7 @@
|
||||
package dict
|
||||
|
||||
import (
|
||||
"github.com/gogf/gf/cmd/gf/v2/internal/cmd/testdata/genctrl-merge/add_new_file/api/dict"
|
||||
"github.com/gogf/gf/cmd/gf/v2/internal/cmd/testdata/genctrl/merge/add_new_file/api/dict"
|
||||
)
|
||||
|
||||
type ControllerV1 struct{}
|
||||
@ -6,7 +6,7 @@ import (
|
||||
"github.com/gogf/gf/v2/errors/gcode"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
|
||||
"github.com/gogf/gf/cmd/gf/v2/internal/cmd/testdata/genctrl-merge/add_new_file/api/dict/v1"
|
||||
"github.com/gogf/gf/cmd/gf/v2/internal/cmd/testdata/genctrl/merge/add_new_file/api/dict/v1"
|
||||
)
|
||||
|
||||
func (c *ControllerV1) DictTypeAddPage(ctx context.Context, req *v1.DictTypeAddPageReq) (res *v1.DictTypeAddPageRes, err error) {
|
||||
@ -6,7 +6,7 @@ import (
|
||||
"github.com/gogf/gf/v2/errors/gcode"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
|
||||
"github.com/gogf/gf/cmd/gf/v2/internal/cmd/testdata/genctrl-merge/add_new_file/api/dict/v1"
|
||||
"github.com/gogf/gf/cmd/gf/v2/internal/cmd/testdata/genctrl/merge/add_new_file/api/dict/v1"
|
||||
)
|
||||
|
||||
func (c *ControllerV1) DictTypeAdd(ctx context.Context, req *v1.DictTypeAddReq) (res *v1.DictTypeAddRes, err error) {
|
||||
15
cmd/gf/internal/cmd/testdata/genctrl/multi/api/admin/article/article_expect.go
vendored
Normal file
15
cmd/gf/internal/cmd/testdata/genctrl/multi/api/admin/article/article_expect.go
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
// =================================================================================
|
||||
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
|
||||
// =================================================================================
|
||||
|
||||
package article
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/gogf/gf/cmd/gf/v2/internal/cmd/testdata/genctrl/multi/api/admin/article/v1"
|
||||
)
|
||||
|
||||
type IArticleV1 interface {
|
||||
Create(ctx context.Context, req *v1.CreateReq) (res *v1.CreateRes, err error)
|
||||
}
|
||||
19
cmd/gf/internal/cmd/testdata/genctrl/multi/api/admin/article/v1/edit.go
vendored
Normal file
19
cmd/gf/internal/cmd/testdata/genctrl/multi/api/admin/article/v1/edit.go
vendored
Normal file
@ -0,0 +1,19 @@
|
||||
// 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 v1
|
||||
|
||||
import "github.com/gogf/gf/v2/frame/g"
|
||||
|
||||
type (
|
||||
// CreateReq add title.
|
||||
CreateReq struct {
|
||||
g.Meta `path:"/article/create" method:"post" tags:"ArticleService"`
|
||||
Title string `v:"required"`
|
||||
}
|
||||
|
||||
CreateRes struct{}
|
||||
)
|
||||
15
cmd/gf/internal/cmd/testdata/genctrl/multi/api/admin/user/user_expect.go
vendored
Normal file
15
cmd/gf/internal/cmd/testdata/genctrl/multi/api/admin/user/user_expect.go
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
// =================================================================================
|
||||
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
|
||||
// =================================================================================
|
||||
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/gogf/gf/cmd/gf/v2/internal/cmd/testdata/genctrl/multi/api/admin/user/v1"
|
||||
)
|
||||
|
||||
type IUserV1 interface {
|
||||
Create(ctx context.Context, req *v1.CreateReq) (res *v1.CreateRes, err error)
|
||||
}
|
||||
19
cmd/gf/internal/cmd/testdata/genctrl/multi/api/admin/user/v1/edit.go
vendored
Normal file
19
cmd/gf/internal/cmd/testdata/genctrl/multi/api/admin/user/v1/edit.go
vendored
Normal file
@ -0,0 +1,19 @@
|
||||
// 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 v1
|
||||
|
||||
import "github.com/gogf/gf/v2/frame/g"
|
||||
|
||||
type (
|
||||
// CreateReq add title.
|
||||
CreateReq struct {
|
||||
g.Meta `path:"/article/create" method:"post" tags:"ArticleService"`
|
||||
Title string `v:"required"`
|
||||
}
|
||||
|
||||
CreateRes struct{}
|
||||
)
|
||||
16
cmd/gf/internal/cmd/testdata/genctrl/multi/api/app/user/user_expect.go
vendored
Normal file
16
cmd/gf/internal/cmd/testdata/genctrl/multi/api/app/user/user_expect.go
vendored
Normal file
@ -0,0 +1,16 @@
|
||||
// =================================================================================
|
||||
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
|
||||
// =================================================================================
|
||||
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/gogf/gf/cmd/gf/v2/internal/cmd/testdata/genctrl/multi/api/app/user/v1"
|
||||
)
|
||||
|
||||
type IUserV1 interface {
|
||||
Create(ctx context.Context, req *v1.CreateReq) (res *v1.CreateRes, err error)
|
||||
Update(ctx context.Context, req *v1.UpdateReq) (res *v1.UpdateRes, err error)
|
||||
}
|
||||
16
cmd/gf/internal/cmd/testdata/genctrl/multi/api/app/user/user_ext/user_ext_expect.go
vendored
Normal file
16
cmd/gf/internal/cmd/testdata/genctrl/multi/api/app/user/user_ext/user_ext_expect.go
vendored
Normal file
@ -0,0 +1,16 @@
|
||||
// =================================================================================
|
||||
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
|
||||
// =================================================================================
|
||||
|
||||
package user_ext
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/gogf/gf/cmd/gf/v2/internal/cmd/testdata/genctrl/multi/api/app/user/user_ext/v1"
|
||||
)
|
||||
|
||||
type IUserExtV1 interface {
|
||||
Create(ctx context.Context, req *v1.CreateReq) (res *v1.CreateRes, err error)
|
||||
Update(ctx context.Context, req *v1.UpdateReq) (res *v1.UpdateRes, err error)
|
||||
}
|
||||
28
cmd/gf/internal/cmd/testdata/genctrl/multi/api/app/user/user_ext/v1/edit.go
vendored
Normal file
28
cmd/gf/internal/cmd/testdata/genctrl/multi/api/app/user/user_ext/v1/edit.go
vendored
Normal file
@ -0,0 +1,28 @@
|
||||
// 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 v1
|
||||
|
||||
import "github.com/gogf/gf/v2/frame/g"
|
||||
|
||||
type (
|
||||
// CreateReq add title.
|
||||
CreateReq struct {
|
||||
g.Meta `path:"/article/create" method:"post" tags:"ArticleService"`
|
||||
Title string `v:"required"`
|
||||
}
|
||||
|
||||
CreateRes struct{}
|
||||
)
|
||||
|
||||
type (
|
||||
UpdateReq struct {
|
||||
g.Meta `path:"/article/update" method:"post" tags:"ArticleService"`
|
||||
Title string `v:"required"`
|
||||
}
|
||||
|
||||
UpdateRes struct{}
|
||||
)
|
||||
28
cmd/gf/internal/cmd/testdata/genctrl/multi/api/app/user/v1/edit.go
vendored
Normal file
28
cmd/gf/internal/cmd/testdata/genctrl/multi/api/app/user/v1/edit.go
vendored
Normal file
@ -0,0 +1,28 @@
|
||||
// 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 v1
|
||||
|
||||
import "github.com/gogf/gf/v2/frame/g"
|
||||
|
||||
type (
|
||||
// CreateReq add title.
|
||||
CreateReq struct {
|
||||
g.Meta `path:"/article/create" method:"post" tags:"ArticleService"`
|
||||
Title string `v:"required"`
|
||||
}
|
||||
|
||||
CreateRes struct{}
|
||||
)
|
||||
|
||||
type (
|
||||
UpdateReq struct {
|
||||
g.Meta `path:"/article/update" method:"post" tags:"ArticleService"`
|
||||
Title string `v:"required"`
|
||||
}
|
||||
|
||||
UpdateRes struct{}
|
||||
)
|
||||
5
cmd/gf/internal/cmd/testdata/genctrl/multi/controller/admin/article/article.go
vendored
Normal file
5
cmd/gf/internal/cmd/testdata/genctrl/multi/controller/admin/article/article.go
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
// =================================================================================
|
||||
// This is auto-generated by GoFrame CLI tool only once. Fill this file as you wish.
|
||||
// =================================================================================
|
||||
|
||||
package article
|
||||
15
cmd/gf/internal/cmd/testdata/genctrl/multi/controller/admin/article/article_new.go
vendored
Normal file
15
cmd/gf/internal/cmd/testdata/genctrl/multi/controller/admin/article/article_new.go
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
// =================================================================================
|
||||
// This is auto-generated by GoFrame CLI tool only once. Fill this file as you wish.
|
||||
// =================================================================================
|
||||
|
||||
package article
|
||||
|
||||
import (
|
||||
"github.com/gogf/gf/cmd/gf/v2/internal/cmd/testdata/genctrl/multi/api/admin/article"
|
||||
)
|
||||
|
||||
type ControllerV1 struct{}
|
||||
|
||||
func NewV1() article.IArticleV1 {
|
||||
return &ControllerV1{}
|
||||
}
|
||||
15
cmd/gf/internal/cmd/testdata/genctrl/multi/controller/admin/article/article_v1_create.go
vendored
Normal file
15
cmd/gf/internal/cmd/testdata/genctrl/multi/controller/admin/article/article_v1_create.go
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
package article
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gcode"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
|
||||
"github.com/gogf/gf/cmd/gf/v2/internal/cmd/testdata/genctrl/multi/api/admin/article/v1"
|
||||
)
|
||||
|
||||
// Create add title.
|
||||
func (c *ControllerV1) Create(ctx context.Context, req *v1.CreateReq) (res *v1.CreateRes, err error) {
|
||||
return nil, gerror.NewCode(gcode.CodeNotImplemented)
|
||||
}
|
||||
5
cmd/gf/internal/cmd/testdata/genctrl/multi/controller/admin/user/user.go
vendored
Normal file
5
cmd/gf/internal/cmd/testdata/genctrl/multi/controller/admin/user/user.go
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
// =================================================================================
|
||||
// This is auto-generated by GoFrame CLI tool only once. Fill this file as you wish.
|
||||
// =================================================================================
|
||||
|
||||
package user
|
||||
15
cmd/gf/internal/cmd/testdata/genctrl/multi/controller/admin/user/user_new.go
vendored
Normal file
15
cmd/gf/internal/cmd/testdata/genctrl/multi/controller/admin/user/user_new.go
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
// =================================================================================
|
||||
// This is auto-generated by GoFrame CLI tool only once. Fill this file as you wish.
|
||||
// =================================================================================
|
||||
|
||||
package user
|
||||
|
||||
import (
|
||||
"github.com/gogf/gf/cmd/gf/v2/internal/cmd/testdata/genctrl/multi/api/admin/user"
|
||||
)
|
||||
|
||||
type ControllerV1 struct{}
|
||||
|
||||
func NewV1() user.IUserV1 {
|
||||
return &ControllerV1{}
|
||||
}
|
||||
15
cmd/gf/internal/cmd/testdata/genctrl/multi/controller/admin/user/user_v1_create.go
vendored
Normal file
15
cmd/gf/internal/cmd/testdata/genctrl/multi/controller/admin/user/user_v1_create.go
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gcode"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
|
||||
"github.com/gogf/gf/cmd/gf/v2/internal/cmd/testdata/genctrl/multi/api/admin/user/v1"
|
||||
)
|
||||
|
||||
// Create add title.
|
||||
func (c *ControllerV1) Create(ctx context.Context, req *v1.CreateReq) (res *v1.CreateRes, err error) {
|
||||
return nil, gerror.NewCode(gcode.CodeNotImplemented)
|
||||
}
|
||||
5
cmd/gf/internal/cmd/testdata/genctrl/multi/controller/app/user/user.go
vendored
Normal file
5
cmd/gf/internal/cmd/testdata/genctrl/multi/controller/app/user/user.go
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
// =================================================================================
|
||||
// This is auto-generated by GoFrame CLI tool only once. Fill this file as you wish.
|
||||
// =================================================================================
|
||||
|
||||
package user
|
||||
5
cmd/gf/internal/cmd/testdata/genctrl/multi/controller/app/user/user_ext/user_ext.go
vendored
Normal file
5
cmd/gf/internal/cmd/testdata/genctrl/multi/controller/app/user/user_ext/user_ext.go
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
// =================================================================================
|
||||
// This is auto-generated by GoFrame CLI tool only once. Fill this file as you wish.
|
||||
// =================================================================================
|
||||
|
||||
package user_ext
|
||||
15
cmd/gf/internal/cmd/testdata/genctrl/multi/controller/app/user/user_ext/user_ext_new.go
vendored
Normal file
15
cmd/gf/internal/cmd/testdata/genctrl/multi/controller/app/user/user_ext/user_ext_new.go
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
// =================================================================================
|
||||
// This is auto-generated by GoFrame CLI tool only once. Fill this file as you wish.
|
||||
// =================================================================================
|
||||
|
||||
package user_ext
|
||||
|
||||
import (
|
||||
"github.com/gogf/gf/cmd/gf/v2/internal/cmd/testdata/genctrl/multi/api/app/user/user_ext"
|
||||
)
|
||||
|
||||
type ControllerV1 struct{}
|
||||
|
||||
func NewV1() user_ext.IUserExtV1 {
|
||||
return &ControllerV1{}
|
||||
}
|
||||
15
cmd/gf/internal/cmd/testdata/genctrl/multi/controller/app/user/user_ext/user_ext_v1_create.go
vendored
Normal file
15
cmd/gf/internal/cmd/testdata/genctrl/multi/controller/app/user/user_ext/user_ext_v1_create.go
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
package user_ext
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gcode"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
|
||||
"github.com/gogf/gf/cmd/gf/v2/internal/cmd/testdata/genctrl/multi/api/app/user/user_ext/v1"
|
||||
)
|
||||
|
||||
// Create add title.
|
||||
func (c *ControllerV1) Create(ctx context.Context, req *v1.CreateReq) (res *v1.CreateRes, err error) {
|
||||
return nil, gerror.NewCode(gcode.CodeNotImplemented)
|
||||
}
|
||||
14
cmd/gf/internal/cmd/testdata/genctrl/multi/controller/app/user/user_ext/user_ext_v1_update.go
vendored
Normal file
14
cmd/gf/internal/cmd/testdata/genctrl/multi/controller/app/user/user_ext/user_ext_v1_update.go
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
package user_ext
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gcode"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
|
||||
"github.com/gogf/gf/cmd/gf/v2/internal/cmd/testdata/genctrl/multi/api/app/user/user_ext/v1"
|
||||
)
|
||||
|
||||
func (c *ControllerV1) Update(ctx context.Context, req *v1.UpdateReq) (res *v1.UpdateRes, err error) {
|
||||
return nil, gerror.NewCode(gcode.CodeNotImplemented)
|
||||
}
|
||||
15
cmd/gf/internal/cmd/testdata/genctrl/multi/controller/app/user/user_new.go
vendored
Normal file
15
cmd/gf/internal/cmd/testdata/genctrl/multi/controller/app/user/user_new.go
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
// =================================================================================
|
||||
// This is auto-generated by GoFrame CLI tool only once. Fill this file as you wish.
|
||||
// =================================================================================
|
||||
|
||||
package user
|
||||
|
||||
import (
|
||||
"github.com/gogf/gf/cmd/gf/v2/internal/cmd/testdata/genctrl/multi/api/app/user"
|
||||
)
|
||||
|
||||
type ControllerV1 struct{}
|
||||
|
||||
func NewV1() user.IUserV1 {
|
||||
return &ControllerV1{}
|
||||
}
|
||||
15
cmd/gf/internal/cmd/testdata/genctrl/multi/controller/app/user/user_v1_create.go
vendored
Normal file
15
cmd/gf/internal/cmd/testdata/genctrl/multi/controller/app/user/user_v1_create.go
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gcode"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
|
||||
"github.com/gogf/gf/cmd/gf/v2/internal/cmd/testdata/genctrl/multi/api/app/user/v1"
|
||||
)
|
||||
|
||||
// Create add title.
|
||||
func (c *ControllerV1) Create(ctx context.Context, req *v1.CreateReq) (res *v1.CreateRes, err error) {
|
||||
return nil, gerror.NewCode(gcode.CodeNotImplemented)
|
||||
}
|
||||
14
cmd/gf/internal/cmd/testdata/genctrl/multi/controller/app/user/user_v1_update.go
vendored
Normal file
14
cmd/gf/internal/cmd/testdata/genctrl/multi/controller/app/user/user_v1_update.go
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gcode"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
|
||||
"github.com/gogf/gf/cmd/gf/v2/internal/cmd/testdata/genctrl/multi/api/app/user/v1"
|
||||
)
|
||||
|
||||
func (c *ControllerV1) Update(ctx context.Context, req *v1.UpdateReq) (res *v1.UpdateRes, err error) {
|
||||
return nil, gerror.NewCode(gcode.CodeNotImplemented)
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
210
container/gtree/gtree_z_k_v_tree_test.go
Normal file
210
container/gtree/gtree_z_k_v_tree_test.go
Normal file
@ -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)
|
||||
})
|
||||
}
|
||||
@ -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 (
|
||||
|
||||
@ -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
|
||||
)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
)
|
||||
|
||||
|
||||
@ -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
|
||||
)
|
||||
|
||||
|
||||
@ -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
|
||||
)
|
||||
|
||||
@ -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 (
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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 (
|
||||
|
||||
@ -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=
|
||||
|
||||
@ -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 (
|
||||
|
||||
@ -47,3 +47,15 @@ func New() gdb.Driver {
|
||||
Driver: mysqlDriver,
|
||||
}
|
||||
}
|
||||
|
||||
// New creates and returns a database object for MariaDB.
|
||||
// It implements the interface of gdb.Driver for extra database driver installation.
|
||||
func (d *Driver) New(core *gdb.Core, node *gdb.ConfigNode) (gdb.DB, error) {
|
||||
mysqlDB, err := d.Driver.New(core, node)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Driver{
|
||||
Driver: mysqlDB.(*mysql.Driver),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@ -29,6 +29,7 @@ SELECT
|
||||
FROM
|
||||
information_schema.COLUMNS AS c
|
||||
LEFT JOIN information_schema.CHECK_CONSTRAINTS AS ch ON c.TABLE_NAME = ch.TABLE_NAME
|
||||
AND c.TABLE_SCHEMA = ch.CONSTRAINT_SCHEMA
|
||||
AND c.COLUMN_NAME = ch.CONSTRAINT_NAME
|
||||
WHERE
|
||||
c.TABLE_SCHEMA = '%s'
|
||||
|
||||
@ -31,6 +31,7 @@ const (
|
||||
|
||||
var (
|
||||
db gdb.DB
|
||||
db2 gdb.DB
|
||||
ctx = context.TODO()
|
||||
)
|
||||
|
||||
@ -59,6 +60,7 @@ func init() {
|
||||
gtest.Error(err)
|
||||
}
|
||||
db = db.Schema(TestSchema1)
|
||||
db2 = db.Schema(TestSchema2)
|
||||
}
|
||||
|
||||
func createTable(table ...string) string {
|
||||
|
||||
@ -1419,3 +1419,80 @@ func Test_Model_HasField(t *testing.T) {
|
||||
t.AssertNil(err)
|
||||
})
|
||||
}
|
||||
|
||||
// https://github.com/gogf/gf/issues/4577
|
||||
// Test TableFields with multiple schemas having same table name with JSON field.
|
||||
// The bug: when JOIN information_schema.CHECK_CONSTRAINTS without schema filter,
|
||||
// MariaDB creates CHECK constraints for JSON fields (json_valid), causing duplicate rows
|
||||
// when multiple schemas have tables with same name and same JSON field name.
|
||||
// This leads to wrong field Index values.
|
||||
func Test_Issue4577_TableFields_MultipleSchema(t *testing.T) {
|
||||
tableName := fmt.Sprintf("test_json_fields_%d", gtime.TimestampNano())
|
||||
|
||||
// Create table with JSON field in schema1 (3 columns)
|
||||
createSQL1 := fmt.Sprintf(`
|
||||
CREATE TABLE %s (
|
||||
id int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
name varchar(45) NULL,
|
||||
data JSON NULL,
|
||||
PRIMARY KEY (id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
`, tableName)
|
||||
|
||||
// Create table with JSON field in schema2 (5 columns - different structure)
|
||||
// This is critical: different column count will cause Index overflow
|
||||
createSQL2 := fmt.Sprintf(`
|
||||
CREATE TABLE %s (
|
||||
id int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
name varchar(45) NULL,
|
||||
extra1 varchar(45) NULL,
|
||||
extra2 varchar(45) NULL,
|
||||
data JSON NULL,
|
||||
PRIMARY KEY (id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
`, tableName)
|
||||
|
||||
// Create table in schema test1 (db) - 3 columns
|
||||
if _, err := db.Exec(ctx, createSQL1); err != nil {
|
||||
gtest.Fatal(err)
|
||||
}
|
||||
defer func() {
|
||||
db.Exec(ctx, fmt.Sprintf("DROP TABLE IF EXISTS %s", tableName))
|
||||
}()
|
||||
|
||||
// Create table in schema test2 (db2) - 5 columns
|
||||
if _, err := db2.Exec(ctx, createSQL2); err != nil {
|
||||
gtest.Fatal(err)
|
||||
}
|
||||
defer func() {
|
||||
db2.Exec(ctx, fmt.Sprintf("DROP TABLE IF EXISTS %s", tableName))
|
||||
}()
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Clear any cached table fields to ensure fresh query
|
||||
db.GetCore().ClearTableFieldsAll(ctx)
|
||||
db2.GetCore().ClearTableFieldsAll(ctx)
|
||||
|
||||
// Get fields from test1 schema (should have 3 columns)
|
||||
fields1, err := db.TableFields(ctx, tableName)
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(fields1), 3)
|
||||
|
||||
// Check the 'data' field's Index - this is the critical check
|
||||
// Without the fix (missing c.TABLE_SCHEMA = ch.CONSTRAINT_SCHEMA),
|
||||
// the 'data' field's Index would be 3 (due to duplicate row from schema2's CHECK constraint)
|
||||
// which is >= 3 and would cause array out of bounds during Scan
|
||||
dataField := fields1["data"]
|
||||
t.Assert(dataField.Index, 2)
|
||||
|
||||
// Verify JSON field type is correctly detected as 'json'
|
||||
t.Assert(fields1["data"].Type, "json")
|
||||
|
||||
// Get fields from test2 schema (should have 5 columns)
|
||||
fields2, err := db2.TableFields(ctx, tableName)
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(fields2), 5)
|
||||
t.Assert(fields2["data"].Index, 4)
|
||||
t.Assert(fields2["data"].Type, "json")
|
||||
})
|
||||
}
|
||||
|
||||
@ -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
|
||||
)
|
||||
|
||||
|
||||
@ -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))
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -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 (
|
||||
|
||||
@ -32,6 +32,7 @@ SELECT
|
||||
FROM
|
||||
information_schema.COLUMNS AS c
|
||||
LEFT JOIN information_schema.CHECK_CONSTRAINTS AS ch ON c.TABLE_NAME = ch.TABLE_NAME
|
||||
AND c.TABLE_SCHEMA = ch.CONSTRAINT_SCHEMA
|
||||
AND c.COLUMN_NAME = ch.CONSTRAINT_NAME
|
||||
WHERE
|
||||
c.TABLE_SCHEMA = '%s'
|
||||
|
||||
@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
@ -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))
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -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 (
|
||||
|
||||
@ -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
|
||||
)
|
||||
|
||||
|
||||
@ -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))
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
)
|
||||
|
||||
@ -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")
|
||||
})
|
||||
}
|
||||
|
||||
@ -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 (
|
||||
|
||||
@ -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))
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user