Veritas Ep. 1: Tu primer proyecto con Gradle
Hello my name is: Veritas
Veritas es una biblioteca de validación de datos que construiremos de manera incremental a lo largo del curso. Su objetivo es ofrecer una API clara y extensible para declarar reglas, componer validaciones y obtener resultados expresivos, facilitando su uso tanto en aplicaciones como en otras bibliotecas. Iremos viendo cómo lograr estos objetivos paso a paso.
Comenzar un proyecto desde cero puede sentirse abrumador, incluso aterrador. Muchas veces no sabemos por dónde empezar, tropezamos en el camino y nos enfrentamos a decisiones que parecen correctas en un momento, pero de las que luego podemos arrepentirnos.
Por eso, Veritas no es solo una biblioteca: es también una historia que contaremos juntos. Una historia que nos servirá de guía para recorrer el proceso completo de creación de una biblioteca desde cero.
Veritas es, en última instancia, una excusa para aprender haciendo: para experimentar, equivocarnos, corregir el rumbo y descubrir cómo se construyen bibliotecas sólidas y reutilizables en el mundo real.
Estructura inicial de proyectos del curso
Comencemos creando una organización básica para los proyectos del curso. Esta carpeta puede ser compartida entre otros proyectos; si decides no usarla, solo tendrás que adaptar las rutas en los comandos posteriores.
$dibs = 'dibs' # raíz de proyectos del curso
$scripts = 'scripts' # scripts de terminal
$path = Join-Path $dibs $scripts
New-Item -ItemType Directory -Path $path -Force | Out-Null
# Alternativas:
# (New-Item -ItemType Directory -Path $path -Force) >$null
# $null = (New-Item -ItemType Directory -Path $path -Force)
-
Join-Path
Join-Path
\
\
/
/
-
New-Item -ItemType Directory
New-Item -ItemType Directory
-
-Path $path
-Path $path
Join-Path
Join-Path
-
-Force
-Force
-
|
|
| Out-Null
| Out-Null
Alternativas:(comando) >$null
(comando) >$null
$null = (comando)
$null = (comando)
|
|
-
DIBS="dibs" # raíz de proyectos del curso
SCRIPTS="scripts" # scripts de terminal
PATH_DIR="$DIBS/$SCRIPTS"
mkdir -p "$PATH_DIR"
-
mkdir -p "$DIBS/$SCRIPTS"
mkdir -p "$DIBS/$SCRIPTS"
-
"$DIBS/$SCRIPTS"
"$DIBS/$SCRIPTS"
(Opcional) Integrar con Git
Si quieres versionar tu estructura desde el inicio, puedes crear el
repositorio local y el remoto en GitLab de forma automatizada. Te
sugiero ejecutar primero un ensayo con
(PowerShell) o
-WhatIf
-WhatIf
(Bash) para validar qué va a ocurrir.
--dry-run
--dry-run
-WhatIf
)
#Requires -Version 7.0
[CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium')]
param(
[ValidateNotNullOrEmpty()]
[Parameter(Mandatory)]
[string] $User,
[ValidateNotNullOrEmpty()]
[string] $RepositoryDirectory = $PWD.Path,
[string] $RepositoryName,
[string] $Prefix
)
$ErrorActionPreference = 'Stop'
# Normalizar ruta (soporta relativa) y validar que sea carpeta
$RepositoryDirectory = (Resolve-Path -LiteralPath $RepositoryDirectory -ErrorAction Stop).Path
$repoDirInfo = Get-Item -LiteralPath $RepositoryDirectory
if (-not $repoDirInfo.PSIsContainer) {
throw "The path '$RepositoryDirectory' is not a directory."
}
# Derivar nombre/prefijo si faltan
if (-not $RepositoryName -or [string]::IsNullOrWhiteSpace($RepositoryName)) {
$RepositoryName = $repoDirInfo.Name.ToLower()
}
if (-not $Prefix -and $repoDirInfo.Parent) {
$Prefix = $repoDirInfo.Parent.Name.ToLower()
}
# Nombre final del proyecto remoto
$target = if ([string]::IsNullOrWhiteSpace($Prefix)) { $RepositoryName } else { "$Prefix-$RepositoryName" }
# Crear remoto en GitLab
if ($PSCmdlet.ShouldProcess($target, 'Create GitLab repository')) {
Push-Location -LiteralPath $repoDirInfo.FullName
try {
git init
glab repo create $target --public
if ($LASTEXITCODE) { throw "glab exited with code $LASTEXITCODE." }
git remote add origin "https://gitlab.com/$User/$target.git"
}
finally {
Pop-Location
}
}
-
#Requires -Version 7.0
#Requires -Version 7.0
-
[CmdletBinding(SupportsShouldProcess, ConfirmImpact='Medium')]
[CmdletBinding(SupportsShouldProcess, ConfirmImpact='Medium')]
-WhatIf
-WhatIf
-Confirm
-Confirm
-WhatIf
-WhatIf
-
$RepositoryDirectory = $PWD.Path
$RepositoryDirectory = $PWD.Path
Resolve-Path -LiteralPath
Resolve-Path -LiteralPath
Get-Item
Get-Item
-
$RepositoryName
$RepositoryName
$Prefix
$Prefix
$Prefix-$RepositoryName
$Prefix-$RepositoryName
-
Push-Location
Push-Location
Pop-Location
Pop-Location
try { ... } finally { ... }
try { ... } finally { ... }
-
glab repo create $target --public
glab repo create $target --public
--private
--private
-
if ($LASTEXITCODE) { throw ... }
if ($LASTEXITCODE) { throw ... }
glab
glab
-
git remote add origin ...
git remote add origin ...
origin
origin
--dry-run
)
#!/usr/bin/env bash
set -euo pipefail
USER=""
REPO_DIR="$(pwd)"
REPO_NAME=""
PREFIX=""
DRY_RUN=false
usage() {
echo "Usage: $0 -u <user> [-d <directory>] [-n <repo_name>] [-p <prefix>] [--dry-run]"
exit 1
}
while [[ $# -gt 0 ]]; do
case $1 in
-u|--user) USER="$2"; shift 2 ;;
-d|--dir) REPO_DIR="$2"; shift 2 ;;
-n|--name) REPO_NAME="$2"; shift 2 ;;
-p|--prefix) PREFIX="$2"; shift 2 ;;
--dry-run) DRY_RUN=true; shift ;;
*) usage ;;
esac
done
if [[ -z "$USER" ]]; then
echo "Error: user is required"; usage
fi
if [[ ! -d "$REPO_DIR" ]]; then
echo "Error: '$REPO_DIR' is not a directory"
exit 1
fi
# Derivar nombre/prefijo
if [[ -z "$REPO_NAME" ]]; then
REPO_NAME="$(basename "$REPO_DIR" | tr '[:upper:]' '[:lower:]')"
fi
if [[ -z "$PREFIX" ]]; then
PARENT="$(dirname "$REPO_DIR")"
PREFIX="$(basename "$PARENT" | tr '[:upper:]' '[:lower:]')"
fi
TARGET="$REPO_NAME"
if [[ -n "$PREFIX" ]]; then
TARGET="$PREFIX-$REPO_NAME"
fi
echo "Repositorio remoto: $TARGET"
run() {
if $DRY_RUN; then
echo "[dry-run] $*"
else
eval "$@"
fi
}
(
cd "$REPO_DIR"
run "git init"
run "glab repo create $TARGET --public"
run "git remote add origin https://gitlab.com/$USER/$TARGET.git"
)
-
#!/usr/bin/env bash
#!/usr/bin/env bash
-
set -euo pipefail
set -euo pipefail
-
-e
-e
-
-u
-u
-
-o pipefail
-o pipefail
-
- Bloque
usage() {}
usage() {}
-u
,-d
,-n
,-p
,--dry-run
). -
while [[ $# -gt 0 ]]; do case ... esac
while [[ $# -gt 0 ]]; do case ... esac
$#
$#
- Si el script se invoca como
./script.sh -u alice -d ./repo
./script.sh -u alice -d ./repo
$#
$#
4
(cada flag y cada valor cuentan). - El bucle
while [[ $# -gt 0 ]]
while [[ $# -gt 0 ]]
- Dentro,
case $1 in ... esac
case $1 in ... esac
$1
$1
shift
shift
$#
$#
De esta forma se recorren y procesan todos los parámetros pasados al script.
-
- Validación: si no se pasa
--user
--user
$REPO_DIR
$REPO_DIR
-
Derivación de nombres:
-
basename
basename
$REPO_NAME
$REPO_NAME
-
dirname
dirname
basename
basename
$PREFIX
$PREFIX
- Ambos se transforman a minúsculas con
tr '[:upper:]' '[:lower:]'
tr '[:upper:]' '[:lower:]'
El nombre final del repo es
$PREFIX-$REPO_NAME
$PREFIX-$REPO_NAME
-
- Se imprime
"Repositorio remoto: $TARGET"
"Repositorio remoto: $TARGET"
-
Función
run() { }
run() { }
- Si está activo
--dry-run
--dry-run
- Si no, lo ejecuta realmente con
eval
eval
$@
$@
-
- Subshell
( cd "$REPO_DIR" ... )
( cd "$REPO_DIR" ... )
-
Dentro del subshell:
-
git init
git init
-
glab repo create $TARGET --public
glab repo create $TARGET --public
--private
--private
-
git remote add origin https://gitlab.com/$USER/$TARGET.git
git remote add origin https://gitlab.com/$USER/$TARGET.git
-
Flujo sugerido (con ensayo primero)
Set-Location 'dibs'
$gitlabUser = '<TU-USUARIO>' # <- Reemplaza aquí
.\scripts\ps1\New-GitLabRepository.ps1 -User $gitlabUser -RepositoryDirectory .\scripts -WhatIf
.\scripts\ps1\New-GitLabRepository.ps1 -User $gitlabUser -RepositoryName index -Prefix dibs -WhatIf
El flag
simula la ejecución sin hacer
nada. Si todo luce bien, repite los comandos sin -WhatIf
-WhatIf
.
-WhatIf
-WhatIf
n
), ya que el script ya se
encarga de eso.
cd dibs
GITLAB_USER="<TU-USUARIO>" # <- Reemplaza aquí
./scripts/sh/new-gitlab-repository.sh -u "$GITLAB_USER" -d ./scripts --dry-run
./scripts/sh/new-gitlab-repository.sh -u "$GITLAB_USER" -n index -p dibs --dry-run
--dry-run
--dry-run
imprime los comandos en lugar de ejecutarlos.
Si luce bien, repite sin
--dry-run
--dry-run
.
.gitignore
adecuado para cada repo; puede ayudarte:
toptal.com/developers/gitignore .
# Commit y push del repo 'dibs/scripts', luego agregarlo como submódulo en 'dibs/index'
Push-Location '.\scripts'
git add .
git commit -m "chore(scripts): initial scaffolding and GitLab integration utilities"
git push -u origin main
Pop-Location
# Commit y push del repo 'dibs/index'
git submodule add "https://gitlab.com/$gitlabUser/dibs-scripts.git" scripts
git add .gitmodules scripts
git commit -m "chore(index): add scripts as submodule"
git push -u origin main
- Primer bloque: versiona y sube el repo
dibs/scripts
. - Segundo bloque: añade
dibs/scripts
como submódulo endibs/index
y lo sube al remoto.
# Commit y push del repo 'dibs/scripts', luego agregarlo como submódulo en 'dibs/index'
(
cd scripts
git add .
git commit -m "chore(scripts): initial scaffolding and GitLab integration utilities"
git push -u origin main
)
# Commit y push del repo 'dibs/index'
git submodule add "https://gitlab.com/$GITLAB_USER/dibs-scripts.git" scripts
git add .gitmodules scripts
git commit -m "chore(index): add scripts as submodule"
git push -u origin main
- El bloque entre paréntesis crea un subshell que entra a
scripts
, hace commit inicial y lo sube a GitLab. - Luego, desde
dibs/index
, se añade el submódulo, actualiza.gitmodules
y se sube todo al remoto.
Un submódulo es un repositorio independiente enlazado dentro de otro repositorio. Se usa cuando quieres mantener el historial y la evolución de un proyecto externo (o de otro repo tuyo) sin copiar su contenido. Así, el repo principal guarda solo una referencia fija a un commit del submódulo. Puedes actualizar esa referencia cuando quieras, pero los cambios en el submódulo se gestionan por separado.
Aprende más sobre submódulos en la documentación de Atlassian.
Inicializar un proyecto con Gradle
Crearemos un script mínimo para generar un esqueleto de proyecto donde tú
elijas. La clave es normalizar la ruta (soporta relativas) y ejecutar
gradle init
en ese directorio. Recomiendo probar 1 con
/ -WhatIf
-WhatIf
antes.
--dry-run
--dry-run
gradle init
El comando gradle init
crea la estructura inicial de un proyecto
Gradle de manera guiada: puedes elegir tipo de proyecto (aplicación, biblioteca,
etc.), DSL (Kotlin o
Groovy) y nombre del proyecto, entre otros.
#Requires -Version 7.0
[CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium')]
param(
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[string] $ProjectRoot,
[string[]] $GradleOptions = @()
)
$ErrorActionPreference = 'Stop'
# Normalize the absolute path based on the current directory
$root = `
if ([IO.Path]::IsPathRooted($ProjectRoot)) { $ProjectRoot } `
else { Join-Path -Path $PWD.Path -ChildPath $ProjectRoot }
# Create directory if missing
if (-not (Test-Path -LiteralPath $root -PathType Container)) {
if ($PSCmdlet.ShouldProcess($root, 'Create directory')) {
New-Item -ItemType Directory -Path $root -Force | Out-Null
}
}
# Run gradle init in the correct path
$gradle = (Get-Command gradle -ErrorAction Stop).Source
if ($PSCmdlet.ShouldProcess($root, 'gradle init')) {
Write-Verbose "Running 'gradle init' in '$root'"
& $gradle -p $root init @GradleOptions
if ($LASTEXITCODE) { throw "Gradle exited with code $LASTEXITCODE." }
}
Esta explicación es más larga a propósito: es nuestro primer script y queremos revisar cada detalle con calma. Más adelante iremos omitiendo lo que ya hayamos aprendido.
-
#Requires -Version 7.0
#Requires -Version 7.0
-
[CmdletBinding(SupportsShouldProcess, ConfirmImpact='Medium')]
[CmdletBinding(SupportsShouldProcess, ConfirmImpact='Medium')]
-WhatIf
-WhatIf
-Confirm
-Confirm
-
param(...)
param(...)
$ProjectRoot
$ProjectRoot
$GradleOptions
$GradleOptions
gradle init
gradle init
-
$ErrorActionPreference = 'Stop'
$ErrorActionPreference = 'Stop'
-
- Comprueba si la carpeta existe con
Test-Path
Test-Path
-
$gradle = (Get-Command gradle -ErrorAction Stop).Source
$gradle = (Get-Command gradle -ErrorAction Stop).Source
-
& $gradle -p $root init @GradleOptions
& $gradle -p $root init @GradleOptions
gradle init
en la carpeta correcta. 8 -
$LASTEXITCODE
$LASTEXITCODE
-
#!/usr/bin/env bash
set -euo pipefail
PROJECT_ROOT="${1:-}"
shift || true
GRADLE_OPTS=( "$@" )
if [[ -z "${PROJECT_ROOT}" ]]; then
echo "Usage: initialize-gradle-project.sh <project-root> [gradle options...]" >&2
exit 1
fi
# Normalize to absolute path
case "${PROJECT_ROOT}" in
/*|?:/*) ROOT="${PROJECT_ROOT}" ;; # Unix abs or Windows drive abs (e.g., C:/)
*) ROOT="${PWD}/${PROJECT_ROOT}" ;;
esac
run () {
if [[ "${DRY_RUN:-false}" == "true" ]]; then
echo "[dry-run] $*"
else
eval "$@"
fi
}
# Create directory if missing
if [[ ! -d "${ROOT}" ]]; then
run "mkdir -p "${ROOT}""
fi
# Ensure gradle exists
if ! command -v gradle >/dev/null 2>&1; then
echo "Gradle not found in PATH" >&2
exit 127
fi
# Run gradle init in the correct path
run "gradle -p "${ROOT}" init ${GRADLE_OPTS[*]}"
Esta explicación es más larga a propósito: es nuestro primer script y queremos revisar cada detalle con calma. Más adelante iremos omitiendo lo que ya hayamos aprendido.
-
#!/usr/bin/env bash
#!/usr/bin/env bash
-
set -euo pipefail
set -euo pipefail
-
-e
-e
-
-u
-u
-
-o pipefail
-o pipefail
-
-
PROJECT_ROOT="${1:-}"
PROJECT_ROOT="${1:-}"
PROJECT_ROOT
PROJECT_ROOT
$1
$1
:-
:-
-
shift || true
shift || true
$1
$1
PROJECT_ROOT
PROJECT_ROOT
|| true
|| true
-
GRADLE_OPTS=( "$@" )
GRADLE_OPTS=( "$@" )
GRADLE_OPTS
GRADLE_OPTS
"$@"
"$@"
gradle init
gradle init
-
if [[ -z "${PROJECT_ROOT}" ]]; then ... fi
if [[ -z "${PROJECT_ROOT}" ]]; then ... fi
PROJECT_ROOT
PROJECT_ROOT
[[ ... ]]
[[ ... ]]
[ ... ]
[ ... ]
-z
-z
>&2
>&2
- Normaliza la ruta del proyecto usando
$PWD
$PWD
$PWD
$PWD
PROJECT_ROOT
PROJECT_ROOT
$PWD
$PWD
-
case ... in ... esac
case ... in ... esac
if / else if
que compara con expresiones regulares simples, no equivale a un pattern matching completo como en Scala o Python.-
/*|?:/*)
/*|?:/*)
/
(ruta absoluta en Unix) o conC:/
(ruta absoluta en Windows), se toma tal cual (ROOT=${PROJECT_ROOT}
ROOT=${PROJECT_ROOT}
-
*)
*)
$PWD
$PWD
;;
;;
case
. -
- Ensayo seguro: activa
DRY_RUN=true
DRY_RUN=true
- Ejecuta
gradle -p "$ROOT" init ${GRADLE_OPTS[*]}
gradle -p "$ROOT" init ${GRADLE_OPTS[*]}
.scriptsps1Initialize-GradleProject.ps1 -ProjectRoot veritas -WhatIf
Notas
-
Al usar
DirectoryInfo
DirectoryInfo
Join-Path -Path $PWD.Path -ChildPath ...
Join-Path -Path $PWD.Path -ChildPath ...
El error original apareció al no usar
Join-Path
Join-Path
What if: Performing the operation "Create Directory" on target "Destination: C:\Users\usuario\veritas". What if: Performing the operation "gradle init" on target "C:\Users\usuario\veritas".
Gracias a
-WhatIf
-WhatIf
--dry-run
--dry-run
C:\Users\...
en vez deE:\...\Dibs
. El ensayo reveló el fallo y la corrección segura fue normalizar la ruta antes degradle init
. -
Puedes ajustar
#Requires
#Requires
-
ConfirmImpact='Medium'
ConfirmImpact='Medium'
-
-
[Parameter(Mandatory)]
[Parameter(Mandatory)]
$ProjectRoot
$ProjectRoot
-
[ValidateNotNullOrEmpty()]
[ValidateNotNullOrEmpty()]
-
[string]
[string]
Ejemplo de uso:
Volver.\Initialize-GradleProject.ps1 -ProjectRoot foo -GradleOptions "--type kotlin-application","--dsl kotlin"
.\Initialize-GradleProject.ps1 -ProjectRoot foo -GradleOptions "--type kotlin-application","--dsl kotlin"
-
-
$PWD
$PWD
-
-LiteralPath
-LiteralPath
-PathType Container
-PathType Container
$PSCmdlet.ShouldProcess
$PSCmdlet.ShouldProcess
-WhatIf
-WhatIf
-Confirm
-Confirm
-
Get-Command
Get-Command
-ErrorAction Stop
-ErrorAction Stop
-
&
&
$gradle
$gradle
-p
-p
@GradleOptions
@GradleOptions
- Por convención, 0 significa éxito; cualquier otro código indica error. Volver
-
[CmdletBinding()]
[CmdletBinding()]
-Verbose
-Verbose
-Debug
-Debug