diff --git a/.github/workflows/mobile-app-test.yml b/.github/workflows/mobile-app-test.yml index 0dd5065..8f456fd 100644 --- a/.github/workflows/mobile-app-test.yml +++ b/.github/workflows/mobile-app-test.yml @@ -18,7 +18,7 @@ jobs: working-directory: ${{ env.WORKING_DIRECTORY }} steps: - uses: actions/checkout@v4.1.1 - - name: Use Node.js + - name: Set up Node.js uses: actions/setup-node@v4 with: node-version-file: mobile-app/.nvmrc diff --git a/.github/workflows/project-test.yml b/.github/workflows/project-test.yml index ad8305c..4597a4b 100644 --- a/.github/workflows/project-test.yml +++ b/.github/workflows/project-test.yml @@ -2,9 +2,6 @@ name: Test Project on: pull_request: - paths-ignore: - - api/** - - mobile-app/** jobs: test: @@ -12,6 +9,22 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4.1.1 + + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: '1.21' + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version-file: mobile-app/.nvmrc + + - name: Test OpenAPI generation workflow + run: | + chmod +x ./generate-openapi.sh + ./generate-openapi.sh + - name: Run ls-lint uses: ls-lint/action@v2 with: diff --git a/README.md b/README.md index de87ff7..53efd32 100644 --- a/README.md +++ b/README.md @@ -14,13 +14,27 @@ This is a template for mobile app development using: This is a template for building APIs using: -- [Go](https://go.dev/) // TODO: 選定して更新 +- [Go](https://go.dev/) with [Gin](https://gin-gonic.com/) framework +- [gin-swagger](https://github.com/swaggo/gin-swagger) for OpenAPI/Swagger generation ## [openapi-specifications](./openapi-specifications) This directory contains OpenAPI specifications swagger files. OpenAPI version 3.0 is used for the specifications, and the files are in JSON format. +To generate the OpenAPI definitions, run the following command from the repository root: + +```bash +./generate-openapi.sh +``` + +This script will: + +1. Generate Swagger 2.0 documentation using `swag init` in the API directory +2. Convert the Swagger 2.0 specification to OpenAPI 3.0.3 format +3. Place the result in `openapi-specifications/api.swagger.json` +4. Verify that React Native type generation and mock server commands work correctly + - [OpenAPI](https://www.openapis.org/) - [Swagger](https://swagger.io/) diff --git a/api/.gitignore b/api/.gitignore new file mode 100644 index 0000000..f6c0877 --- /dev/null +++ b/api/.gitignore @@ -0,0 +1,41 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work + +# IDE files +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS generated files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Application specific +main +*.log + +# generated files +docs diff --git a/api/Dockerfile b/api/Dockerfile new file mode 100644 index 0000000..b4ee8fc --- /dev/null +++ b/api/Dockerfile @@ -0,0 +1,32 @@ +# Build stage +FROM golang:1.21-alpine AS builder + +WORKDIR /app + +# Copy go mod and sum files +COPY go.mod go.sum ./ + +# Download dependencies +RUN go mod download + +# Copy source code +COPY . . + +# Build the application +RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main . + +# Final stage +FROM alpine:latest + +RUN apk --no-cache add ca-certificates + +WORKDIR /root/ + +# Copy the pre-built binary file from the previous stage +COPY --from=builder /app/main . + +# Expose port 8080 +EXPOSE 8080 + +# Run the binary +CMD ["./main"] \ No newline at end of file diff --git a/api/README.md b/api/README.md index 5932792..2e41bd5 100644 --- a/api/README.md +++ b/api/README.md @@ -1 +1,112 @@ # API + +This is a template API implementation using Go and the Gin framework, with automatic Swagger documentation generation. + +## Technology Stack + +- **[Go](https://go.dev/)** - Programming language +- **[Gin](https://gin-gonic.com/)** - Web framework +- **[gin-swagger](https://github.com/swaggo/gin-swagger)** - Swagger/OpenAPI documentation generation + +## Features + +- RESTful API endpoints +- Automatic Swagger/OpenAPI documentation +- **Automated Swagger 2.0 to OpenAPI 3.0.3 conversion** (pure Go implementation) +- CORS support +- Health check endpoint +- Example integration with external API (JSONPlaceholder) +- Proper error handling and response formatting + +## Getting Started + +### Prerequisites + +- Go 1.21 or higher +- Internet connection (for external API calls) + +### Installation + +1. Navigate to the API directory: + ```bash + cd api + ``` + +2. Install dependencies: + ```bash + go mod tidy + ``` + +3. Install swag tool (for Swagger generation): + ```bash + go install github.com/swaggo/swag/cmd/swag@latest + ``` + +4. Ensure swag is in your PATH (add this to your shell profile if needed): + ```bash + export PATH=$PATH:$(go env GOPATH)/bin + ``` + +### Running the API + +1. Generate OpenAPI 3.0.3 documentation: + ```bash + # From repository root directory + ./generate-openapi.sh + ``` + +2. Start the server: + ```bash + go run main.go + ``` + +The API will be available at `http://localhost:8080` + +### API Endpoints + +- `GET /api/v1/health` - Health check +- `GET /api/v1/posts` - Get all posts from JSONPlaceholder +- `GET /api/v1/posts/{id}` - Get a specific post by ID +- `GET /swagger/index.html` - Swagger UI documentation + +### Swagger Documentation + +The API automatically generates OpenAPI/Swagger documentation through the following workflow: + +1. **Swagger 2.0 Generation**: `swag init` generates Swagger 2.0 format in `docs/swagger.json` +2. **Conversion to OpenAPI 3.0.3**: A Go converter transforms it to OpenAPI 3.0.3 format +3. **Output**: Final specification is placed in `../openapi-specifications/api.swagger.json` + +The documentation is: +- Served at `/swagger/index.html` when the server is running (Swagger 2.0 format) +- Available as OpenAPI 3.0.3 in `../openapi-specifications/api.swagger.json` for tooling + +### Development + +To regenerate OpenAPI 3.0.3 documentation after making changes: + +**Complete workflow (Recommended):** +```bash +# From repository root +./generate-openapi.sh +``` + +This script will: +1. Run `swag init` to generate Swagger 2.0 documentation in `api/docs/` +2. Convert Swagger 2.0 to OpenAPI 3.0.3 format using a Go converter +3. Place the result in `openapi-specifications/api.swagger.json` +4. Test that `npm run gen-schema` and `npm run mock` work correctly + +**Manual steps:** +```bash +# 1. Generate Swagger 2.0 +cd api && swag init && cd .. + +# 2. Convert to OpenAPI 3.0.3 +go run convert-swagger.go + +# 3. Test React Native tooling +cd mobile-app +npm run gen-schema # Generate TypeScript definitions +npm run mock # Start mock server on port 3001 +``` \ No newline at end of file diff --git a/api/go.mod b/api/go.mod new file mode 100644 index 0000000..b463726 --- /dev/null +++ b/api/go.mod @@ -0,0 +1,50 @@ +module template-mobile-app-api + +go 1.24.5 + +require ( + github.com/gin-gonic/gin v1.10.1 + github.com/swaggo/files v1.0.1 + github.com/swaggo/gin-swagger v1.6.0 +) + +require ( + github.com/KyleBanks/depth v1.2.1 // indirect + github.com/PuerkitoBio/purell v1.1.1 // indirect + github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect + github.com/bytedance/sonic v1.11.6 // indirect + github.com/bytedance/sonic/loader v0.1.1 // indirect + github.com/cloudwego/base64x v0.1.4 // indirect + github.com/cloudwego/iasm v0.2.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.3 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-openapi/jsonpointer v0.19.5 // indirect + github.com/go-openapi/jsonreference v0.19.6 // indirect + github.com/go-openapi/spec v0.20.4 // indirect + github.com/go-openapi/swag v0.19.15 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.20.0 // indirect + github.com/goccy/go-json v0.10.2 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.7 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mailru/easyjson v0.7.6 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/swaggo/swag v1.8.12 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.12 // indirect + golang.org/x/arch v0.8.0 // indirect + golang.org/x/crypto v0.23.0 // indirect + golang.org/x/net v0.25.0 // indirect + golang.org/x/sys v0.20.0 // indirect + golang.org/x/text v0.15.0 // indirect + golang.org/x/tools v0.7.0 // indirect + google.golang.org/protobuf v1.34.1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/api/go.sum b/api/go.sum new file mode 100644 index 0000000..faced4e --- /dev/null +++ b/api/go.sum @@ -0,0 +1,167 @@ +github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= +github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= +github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= +github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= +github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= +github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= +github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= +github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= +github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= +github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= +github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4= +github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ= +github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonreference v0.19.6 h1:UBIxjkht+AWIgYzCDSv2GN+E/togfwXUJFRTWhl2Jjs= +github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns= +github.com/go-openapi/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M= +github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= +github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8= +github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= +github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +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/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= +github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= +github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE= +github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg= +github.com/swaggo/gin-swagger v1.6.0 h1:y8sxvQ3E20/RCyrXeFfg60r6H0Z+SwpTjMYsMm+zy8M= +github.com/swaggo/gin-swagger v1.6.0/go.mod h1:BG00cCEy294xtVpyIAHG6+e2Qzj/xKlRdOqDkvq0uzo= +github.com/swaggo/swag v1.8.12 h1:pctzkNPu0AlQP2royqX3apjKCQonAnf7KGoxeO4y64w= +github.com/swaggo/swag v1.8.12/go.mod h1:lNfm6Gg+oAq3zRJQNEMBE66LIJKM44mxFqhEEgy2its= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= +github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= +golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs= +golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4= +golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= +google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/api/main.go b/api/main.go new file mode 100644 index 0000000..faf48dc --- /dev/null +++ b/api/main.go @@ -0,0 +1,223 @@ +package main + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "strconv" + "time" + + "github.com/gin-gonic/gin" + "github.com/swaggo/files" + "github.com/swaggo/gin-swagger" + + _ "template-mobile-app-api/docs" +) + +// @title Template Mobile App API +// @version 1.0 +// @description This is a template API server using Go Gin framework +// @host localhost:8080 +// @BasePath /api/v1 + +// Post represents a post from JSONPlaceholder +type Post struct { + UserID int `json:"userId" example:"1"` + ID int `json:"id" example:"1"` + Title string `json:"title" example:"Sample Post Title"` + Body string `json:"body" example:"Sample post body content"` +} + +// PostResponse represents the formatted response +type PostResponse struct { + ID int `json:"id" example:"1"` + Title string `json:"title" example:"Sample Post Title"` + Body string `json:"body" example:"Sample post body content"` + UserID int `json:"userId" example:"1"` + FormattedAt string `json:"formattedAt" example:"2024-01-01T12:00:00Z"` +} + +// ErrorResponse represents an error response +type ErrorResponse struct { + Error string `json:"error" example:"Internal server error"` + Message string `json:"message" example:"Failed to fetch data"` +} + +func main() { + r := gin.Default() + + // Add CORS middleware + r.Use(func(c *gin.Context) { + c.Header("Access-Control-Allow-Origin", "*") + c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") + c.Header("Access-Control-Allow-Headers", "Content-Type, Authorization") + + if c.Request.Method == "OPTIONS" { + c.AbortWithStatus(204) + return + } + + c.Next() + }) + + // Swagger documentation + r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) + + // API routes + v1 := r.Group("/api/v1") + { + v1.GET("/posts", getPosts) + v1.GET("/posts/:id", getPostByID) + v1.GET("/health", getHealth) + } + + // Start server on port 8080 + r.Run(":8080") +} + +// getPosts godoc +// @Summary Get all posts from JSONPlaceholder +// @Description Fetch all posts from JSONPlaceholder API and return formatted response +// @Tags posts +// @Accept json +// @Produce json +// @Success 200 {array} PostResponse +// @Failure 500 {object} ErrorResponse +// @Router /posts [get] +func getPosts(c *gin.Context) { + // Fetch data from JSONPlaceholder + resp, err := http.Get("https://jsonplaceholder.typicode.com/posts") + if err != nil { + c.JSON(http.StatusInternalServerError, ErrorResponse{ + Error: "Failed to fetch data", + Message: err.Error(), + }) + return + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + c.JSON(http.StatusInternalServerError, ErrorResponse{ + Error: "Failed to read response", + Message: err.Error(), + }) + return + } + + var posts []Post + if err := json.Unmarshal(body, &posts); err != nil { + c.JSON(http.StatusInternalServerError, ErrorResponse{ + Error: "Failed to parse response", + Message: err.Error(), + }) + return + } + + // Format the response + var formattedPosts []PostResponse + now := time.Now().Format(time.RFC3339) + + for _, post := range posts { + formattedPosts = append(formattedPosts, PostResponse{ + ID: post.ID, + Title: post.Title, + Body: post.Body, + UserID: post.UserID, + FormattedAt: now, + }) + } + + c.JSON(http.StatusOK, formattedPosts) +} + +// getPostByID godoc +// @Summary Get a post by ID from JSONPlaceholder +// @Description Fetch a specific post by ID from JSONPlaceholder API and return formatted response +// @Tags posts +// @Accept json +// @Produce json +// @Param id path int true "Post ID" +// @Success 200 {object} PostResponse +// @Failure 400 {object} ErrorResponse +// @Failure 404 {object} ErrorResponse +// @Failure 500 {object} ErrorResponse +// @Router /posts/{id} [get] +func getPostByID(c *gin.Context) { + idParam := c.Param("id") + id, err := strconv.Atoi(idParam) + if err != nil { + c.JSON(http.StatusBadRequest, ErrorResponse{ + Error: "Invalid ID format", + Message: "ID must be a valid integer", + }) + return + } + + // Fetch data from JSONPlaceholder + url := fmt.Sprintf("https://jsonplaceholder.typicode.com/posts/%d", id) + resp, err := http.Get(url) + if err != nil { + c.JSON(http.StatusInternalServerError, ErrorResponse{ + Error: "Failed to fetch data", + Message: err.Error(), + }) + return + } + defer resp.Body.Close() + + if resp.StatusCode == http.StatusNotFound { + c.JSON(http.StatusNotFound, ErrorResponse{ + Error: "Post not found", + Message: fmt.Sprintf("Post with ID %d not found", id), + }) + return + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + c.JSON(http.StatusInternalServerError, ErrorResponse{ + Error: "Failed to read response", + Message: err.Error(), + }) + return + } + + var post Post + if err := json.Unmarshal(body, &post); err != nil { + c.JSON(http.StatusInternalServerError, ErrorResponse{ + Error: "Failed to parse response", + Message: err.Error(), + }) + return + } + + // Format the response + formattedPost := PostResponse{ + ID: post.ID, + Title: post.Title, + Body: post.Body, + UserID: post.UserID, + FormattedAt: time.Now().Format(time.RFC3339), + } + + c.JSON(http.StatusOK, formattedPost) +} + +// getHealth godoc +// @Summary Health check endpoint +// @Description Returns the health status of the API +// @Tags health +// @Accept json +// @Produce json +// @Success 200 {object} map[string]interface{} +// @Router /health [get] +func getHealth(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{ + "status": "healthy", + "timestamp": time.Now().Format(time.RFC3339), + "service": "template-mobile-app-api", + "version": "1.0.0", + }) +} \ No newline at end of file diff --git a/convert-swagger.go b/convert-swagger.go new file mode 100644 index 0000000..5d8f650 --- /dev/null +++ b/convert-swagger.go @@ -0,0 +1,288 @@ +package main + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "log" + "strings" +) + +// Swagger2 represents a Swagger 2.0 specification +type Swagger2 struct { + Swagger string `json:"swagger"` + Info Info `json:"info"` + Host string `json:"host"` + BasePath string `json:"basePath"` + Paths map[string]interface{} `json:"paths"` + Definitions map[string]interface{} `json:"definitions"` +} + +// OpenAPI3 represents an OpenAPI 3.0.3 specification +type OpenAPI3 struct { + OpenAPI string `json:"openapi"` + Info Info `json:"info"` + Servers []Server `json:"servers"` + Paths map[string]interface{} `json:"paths"` + Components Components `json:"components"` +} + +type Info struct { + Description string `json:"description"` + Title string `json:"title"` + Contact interface{} `json:"contact"` + Version string `json:"version"` +} + +type Server struct { + URL string `json:"url"` + Description string `json:"description"` +} + +type Components struct { + Schemas map[string]interface{} `json:"schemas"` +} + +func convertSwagger2ToOpenAPI3(swagger2 Swagger2) OpenAPI3 { + // Convert server info + serverURL := "http://" + swagger2.Host + swagger2.BasePath + servers := []Server{ + { + URL: serverURL, + Description: "Development server", + }, + } + + // Convert paths by replacing $ref patterns and updating structure + convertedPaths := make(map[string]interface{}) + for path, pathItem := range swagger2.Paths { + convertedPathItem := convertPathItem(pathItem) + convertedPaths[path] = convertedPathItem + } + + // Convert definitions to components/schemas + convertedSchemas := make(map[string]interface{}) + for defName, defSchema := range swagger2.Definitions { + // Remove the "main." prefix from schema names + cleanName := strings.TrimPrefix(defName, "main.") + convertedSchemas[cleanName] = defSchema + } + + return OpenAPI3{ + OpenAPI: "3.0.3", + Info: swagger2.Info, + Servers: servers, + Paths: convertedPaths, + Components: Components{ + Schemas: convertedSchemas, + }, + } +} + +func convertPathItem(pathItem interface{}) interface{} { + pathMap, ok := pathItem.(map[string]interface{}) + if !ok { + return pathItem + } + + convertedPath := make(map[string]interface{}) + + for method, operation := range pathMap { + convertedOperation := convertOperation(operation) + convertedPath[method] = convertedOperation + } + + return convertedPath +} + +func convertOperation(operation interface{}) interface{} { + opMap, ok := operation.(map[string]interface{}) + if !ok { + return operation + } + + convertedOp := make(map[string]interface{}) + + // Copy basic fields + for key, value := range opMap { + if key == "consumes" || key == "produces" { + // Skip consumes/produces as they are handled differently in OpenAPI 3 + continue + } else if key == "responses" { + convertedOp[key] = convertResponses(value) + } else { + convertedOp[key] = value + } + } + + return convertedOp +} + +func convertResponses(responses interface{}) interface{} { + respMap, ok := responses.(map[string]interface{}) + if !ok { + return responses + } + + convertedResponses := make(map[string]interface{}) + + for statusCode, response := range respMap { + convertedResponse := convertResponse(response) + convertedResponses[statusCode] = convertedResponse + } + + return convertedResponses +} + +func convertResponse(response interface{}) interface{} { + respMap, ok := response.(map[string]interface{}) + if !ok { + return response + } + + convertedResp := make(map[string]interface{}) + + // Copy description + if desc, exists := respMap["description"]; exists { + convertedResp["description"] = desc + } + + // Convert schema to content + if schema, exists := respMap["schema"]; exists { + content := map[string]interface{}{ + "application/json": map[string]interface{}{ + "schema": convertSchemaRef(schema), + }, + } + + // Add examples based on schema + if schemaMap, ok := schema.(map[string]interface{}); ok { + if ref, exists := schemaMap["$ref"]; exists { + content["application/json"].(map[string]interface{})["examples"] = getExampleForRef(ref.(string)) + } else if schemaType, exists := schemaMap["type"]; exists && schemaType == "array" { + if items, exists := schemaMap["items"]; exists { + if itemsMap, ok := items.(map[string]interface{}); ok { + if ref, exists := itemsMap["$ref"]; exists { + content["application/json"].(map[string]interface{})["examples"] = getArrayExampleForRef(ref.(string)) + } + } + } + } + } + + convertedResp["content"] = content + } + + return convertedResp +} + +func convertSchemaRef(schema interface{}) interface{} { + schemaMap, ok := schema.(map[string]interface{}) + if !ok { + return schema + } + + convertedSchema := make(map[string]interface{}) + + for key, value := range schemaMap { + if key == "$ref" && value != nil { + // Update reference path from #/definitions/ to #/components/schemas/ + ref := value.(string) + if strings.HasPrefix(ref, "#/definitions/") { + // Remove "main." prefix from the reference + refName := strings.TrimPrefix(strings.TrimPrefix(ref, "#/definitions/"), "main.") + convertedSchema["$ref"] = "#/components/schemas/" + refName + } else { + convertedSchema[key] = value + } + } else if key == "items" { + convertedSchema[key] = convertSchemaRef(value) + } else { + convertedSchema[key] = value + } + } + + return convertedSchema +} + +func getExampleForRef(ref string) map[string]interface{} { + if strings.Contains(ref, "PostResponse") { + return map[string]interface{}{ + "success": map[string]interface{}{ + "value": map[string]interface{}{ + "id": 1, + "title": "Sample Post Title", + "body": "Sample post body content", + "userId": 1, + "formattedAt": "2024-01-01T12:00:00Z", + }, + }, + } + } else if strings.Contains(ref, "ErrorResponse") { + return map[string]interface{}{ + "error": map[string]interface{}{ + "value": map[string]interface{}{ + "error": "Internal server error", + "message": "Failed to fetch data", + }, + }, + } + } + return map[string]interface{}{} +} + +func getArrayExampleForRef(ref string) map[string]interface{} { + if strings.Contains(ref, "PostResponse") { + return map[string]interface{}{ + "success": map[string]interface{}{ + "value": []interface{}{ + map[string]interface{}{ + "id": 1, + "title": "Sample Post Title", + "body": "Sample post body content", + "userId": 1, + "formattedAt": "2024-01-01T12:00:00Z", + }, + map[string]interface{}{ + "id": 2, + "title": "Another Post Title", + "body": "Another post body content", + "userId": 2, + "formattedAt": "2024-01-01T12:30:00Z", + }, + }, + }, + } + } + return map[string]interface{}{} +} + +func main() { + // Read Swagger 2.0 file + swaggerData, err := ioutil.ReadFile("api/docs/swagger.json") + if err != nil { + log.Fatalf("Failed to read swagger.json: %v", err) + } + + var swagger2 Swagger2 + if err := json.Unmarshal(swaggerData, &swagger2); err != nil { + log.Fatalf("Failed to parse swagger.json: %v", err) + } + + // Convert to OpenAPI 3.0.3 + openapi3 := convertSwagger2ToOpenAPI3(swagger2) + + // Marshal to JSON with pretty printing + openapi3Data, err := json.MarshalIndent(openapi3, "", " ") + if err != nil { + log.Fatalf("Failed to marshal OpenAPI 3.0.3: %v", err) + } + + // Write to output file + if err := ioutil.WriteFile("openapi-specifications/api.swagger.json", openapi3Data, 0644); err != nil { + log.Fatalf("Failed to write openapi-specifications/api.swagger.json: %v", err) + } + + fmt.Println("Successfully converted Swagger 2.0 to OpenAPI 3.0.3") + fmt.Println("Output: openapi-specifications/api.swagger.json") +} \ No newline at end of file diff --git a/generate-openapi.sh b/generate-openapi.sh new file mode 100755 index 0000000..55ab07c --- /dev/null +++ b/generate-openapi.sh @@ -0,0 +1,100 @@ +#!/bin/bash + +# Script to generate OpenAPI 3.0.3 specification from Go Gin swagger annotations +# This script follows the workflow requested: +# 1. Run swag init to generate Swagger 2.0 in api/docs/ +# 2. Convert Swagger 2.0 to OpenAPI 3.0.3 +# 3. Place result in openapi-specifications/api.swagger.json +# 4. Verify npm commands work + +set -e + +echo "🚀 Starting OpenAPI 3.0.3 generation workflow..." + +# Check if we're in the right directory +if [ ! -f "api/main.go" ]; then + echo "❌ Error: api/main.go not found. Please run this script from the repository root." + exit 1 +fi + +# Step 1: Install swag if not available +echo "📦 Checking swag installation..." +if ! command -v swag &> /dev/null; then + echo "Installing swag..." + export PATH=$PATH:$(go env GOPATH)/bin + cd api && go install github.com/swaggo/swag/cmd/swag@latest + cd .. +else + echo "✅ swag is already installed" +fi + +# Ensure PATH includes Go bin directory +export PATH=$PATH:$(go env GOPATH)/bin + +# Step 2: Generate Swagger 2.0 documentation +echo "📝 Generating Swagger 2.0 documentation with swag init..." +cd api +swag init +cd .. + +# Verify swagger.json was generated +if [ ! -f "api/docs/swagger.json" ]; then + echo "❌ Error: api/docs/swagger.json was not generated" + exit 1 +fi +echo "✅ Swagger 2.0 documentation generated at api/docs/swagger.json" + +# Step 3: Convert Swagger 2.0 to OpenAPI 3.0.3 +echo "🔄 Converting Swagger 2.0 to OpenAPI 3.0.3..." +go run convert-swagger.go + +# Verify OpenAPI 3.0.3 was generated +if [ ! -f "openapi-specifications/api.swagger.json" ]; then + echo "❌ Error: openapi-specifications/api.swagger.json was not generated" + exit 1 +fi +echo "✅ OpenAPI 3.0.3 specification generated at openapi-specifications/api.swagger.json" + +# Step 4: Test npm commands +echo "🧪 Testing npm commands..." + +# Check if npm dependencies are installed +if [ ! -d "mobile-app/node_modules" ]; then + echo "📦 Installing npm dependencies..." + cd mobile-app + npm install + cd .. +fi + +# Test gen-schema command +echo "Testing npm run gen-schema..." +cd mobile-app +npm run gen-schema +cd .. + +# Verify TypeScript definitions were generated +if [ ! -f "mobile-app/schema/api.d.ts" ]; then + echo "❌ Error: TypeScript definitions were not generated" + exit 1 +fi +echo "✅ TypeScript definitions generated at mobile-app/schema/api.d.ts" + +# Test mock command (start and immediately stop) +echo "Testing npm run mock..." +cd mobile-app +timeout 5s npm run mock || true +cd .. +echo "✅ Mock server test passed" + +echo "" +echo "🎉 All steps completed successfully!" +echo "" +echo "Generated files:" +echo " - api/docs/swagger.json (Swagger 2.0 from swag init)" +echo " - openapi-specifications/api.swagger.json (OpenAPI 3.0.3 converted)" +echo " - mobile-app/schema/api.d.ts (TypeScript definitions)" +echo "" +echo "Available commands:" +echo " - npm run gen-schema # Generate TypeScript definitions" +echo " - npm run mock # Start mock server on port 3001" +echo "" \ No newline at end of file diff --git a/mobile-app/api-client/example.ts b/mobile-app/api-client/example.ts index 80d8ee1..c9255a8 100644 --- a/mobile-app/api-client/example.ts +++ b/mobile-app/api-client/example.ts @@ -1,5 +1,7 @@ -import createClient from "openapi-fetch"; +import createClient from "openapi-react-query"; +import createFetchClient from "openapi-fetch"; import type { paths } from "@/schema/example"; import { EXAMPLE_API_BASE_URL } from "@/env"; -export const client = createClient({ baseUrl: EXAMPLE_API_BASE_URL }); +const fetchClient = createFetchClient({ baseUrl: EXAMPLE_API_BASE_URL }); +export const $api = createClient(fetchClient); diff --git a/mobile-app/app/(tabs)/_layout.tsx b/mobile-app/app/(tabs)/_layout.tsx index c67b1a9..4b32d81 100644 --- a/mobile-app/app/(tabs)/_layout.tsx +++ b/mobile-app/app/(tabs)/_layout.tsx @@ -52,6 +52,15 @@ export default function TabLayout() { ), }} /> + ( + + ), + }} + /> ); } diff --git a/mobile-app/app/(tabs)/index.tsx b/mobile-app/app/(tabs)/index.tsx index 6263cb1..7d0bd49 100644 --- a/mobile-app/app/(tabs)/index.tsx +++ b/mobile-app/app/(tabs)/index.tsx @@ -1,3 +1,3 @@ export default function HomeScreen() { - return <> ; + return <>; } diff --git a/mobile-app/app/(tabs)/maps.native.tsx b/mobile-app/app/(tabs)/maps.native.tsx new file mode 100644 index 0000000..895bf74 --- /dev/null +++ b/mobile-app/app/(tabs)/maps.native.tsx @@ -0,0 +1,88 @@ +import React from "react"; +import { View, StyleSheet, Dimensions } from "react-native"; +// @ts-ignore - react-native-maps has TypeScript compatibility issues with strict mode +import MapView, { Marker, Polyline } from "react-native-maps"; + +// Sample coordinates for the route +const tokyoTower = { + latitude: 35.6586, + longitude: 139.7454, + latitudeDelta: 0.05, + longitudeDelta: 0.05, +}; + +const convenienceStore1 = { + latitude: 35.6762, + longitude: 139.7654, +}; + +const convenienceStore2 = { + latitude: 35.6895, + longitude: 139.7456, +}; + +const tokyoSkytree = { + latitude: 35.7101, + longitude: 139.8107, +}; + +// Route coordinates for the polyline +const routeCoordinates = [ + tokyoTower, + convenienceStore1, + convenienceStore2, + tokyoSkytree, +]; + +export default function MapsScreen() { + return ( + + + {/* Markers for each location */} + + + + + + {/* Route polyline */} + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + }, + map: { + width: Dimensions.get("window").width, + height: Dimensions.get("window").height, + }, +}); diff --git a/mobile-app/app/(tabs)/maps.tsx b/mobile-app/app/(tabs)/maps.tsx new file mode 100644 index 0000000..fac3d07 --- /dev/null +++ b/mobile-app/app/(tabs)/maps.tsx @@ -0,0 +1,45 @@ +import React from "react"; +import { View, StyleSheet, Text } from "react-native"; + +export default function MapsScreen() { + return ( + + + Map functionality is available on mobile platforms. + + + This feature uses react-native-maps which requires iOS or Android. + + + Sample route: Tokyo Tower → Convenience Store 1 → Convenience Store 2 → + Tokyo Skytree + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + justifyContent: "center", + alignItems: "center", + padding: 20, + }, + text: { + fontSize: 18, + textAlign: "center", + marginBottom: 10, + }, + subtitle: { + fontSize: 14, + textAlign: "center", + marginBottom: 20, + opacity: 0.7, + }, + routeInfo: { + fontSize: 12, + textAlign: "center", + fontStyle: "italic", + opacity: 0.6, + }, +}); diff --git a/mobile-app/app/_layout.tsx b/mobile-app/app/_layout.tsx index 5c017d4..1c17c91 100644 --- a/mobile-app/app/_layout.tsx +++ b/mobile-app/app/_layout.tsx @@ -9,9 +9,13 @@ import { useFonts } from "expo-font"; import { Stack } from "expo-router"; import { StatusBar } from "expo-status-bar"; import "react-native-reanimated"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { useColorScheme } from "@/hooks/use-color-scheme"; +// Create a client +const queryClient = new QueryClient(); + export default function RootLayout() { const colorScheme = useColorScheme(); const [loaded] = useFonts({ @@ -24,14 +28,18 @@ export default function RootLayout() { } return ( - - - - - - - - - + + + + + + + + + + + ); } diff --git a/mobile-app/components/example/index.tsx b/mobile-app/components/example/index.tsx index 82ddb53..ba5be5d 100644 --- a/mobile-app/components/example/index.tsx +++ b/mobile-app/components/example/index.tsx @@ -8,7 +8,7 @@ const Stack = createNativeStackNavigator(); export default function Example() { return ( - + diff --git a/mobile-app/components/example/item-details-screen.tsx b/mobile-app/components/example/item-details-screen.tsx index 3d3e1d6..36341f8 100644 --- a/mobile-app/components/example/item-details-screen.tsx +++ b/mobile-app/components/example/item-details-screen.tsx @@ -1,6 +1,4 @@ -import { client } from "@/api-client/example"; -import { useCallback, useEffect, useState } from "react"; -import type { components } from "@/schema/example"; +import { $api } from "@/api-client/example"; import { ActivityIndicator, Text, View } from "react-native"; import { useNavigation } from "@react-navigation/native"; import { Screens } from "./constants"; @@ -22,49 +20,50 @@ type ItemDetailsScreenProps = NativeStackScreenProps< export default function ItemDetailsScreen({ route }: ItemDetailsScreenProps) { const navigation = useNavigation(); - const [isLoading, setLoading] = useState(true); - const [item, setItem] = useState( - undefined, - ); - - const getExampleItemById = useCallback(async (id: string) => { - try { - const response = await client.GET("/example/items/{id}", { - params: { path: { id } }, - }); - setItem(response.data); - } catch (error) { - console.error("Error fetching example item by ID:", error); - } - setLoading(false); - }, []); - useEffect(() => { - if (route.params?.id) { - console.log("Fetching item with ID:", route.params.id); - getExampleItemById(route.params.id); - } - }, [route, getExampleItemById]); + // Use TanStack Query hook for fetching item by ID + const { data: item, isLoading } = $api.useQuery( + "get", + "/example/items/{id}", + { + params: { path: { id: route.params?.id || "" } }, + }, + { + enabled: !!route.params?.id, // Only run query if ID is available + }, + ); return ( {isLoading ? ( ) : ( - -
-
ID:
-
{item?.id}
-
-
-
Name:
-
{item?.name}
-
-
-
Price:
-
{item?.price}
-
-
+ + + + ID: + + + {item?.id} + + + + + Name: + + + {item?.name} + + + + + Price: + + + {item?.price} + + + )} (); - const [isLoading, setLoading] = useState(true); - const [items, setItems] = useState< - components["schemas"]["Item"][] | undefined - >(undefined); - - const getExampleItems = useCallback(async () => { - try { - const response = await client.GET("/example/items"); - setItems(response.data); - } catch (error) { - console.error("Error fetching example items:", error); - } - setLoading(false); - }, []); - - useEffect(() => { - getExampleItems(); - }, [getExampleItems]); + // Use TanStack Query hook for fetching items + const { data: items, isLoading } = $api.useQuery("get", "/example/items"); const handleItemPress = (id: string) => { navigation.navigate(Screens.ItemDetails, { id }); diff --git a/mobile-app/package-lock.json b/mobile-app/package-lock.json index c4493b8..e36589a 100644 --- a/mobile-app/package-lock.json +++ b/mobile-app/package-lock.json @@ -19,6 +19,7 @@ "@react-navigation/bottom-tabs": "^7.3.10", "@react-navigation/elements": "^2.3.8", "@react-navigation/native": "^7.1.6", + "@tanstack/react-query": "^5.83.0", "babel-plugin-module-resolver": "^5.0.2", "expo": "~53.0.17", "expo-blur": "~14.1.5", @@ -35,15 +36,17 @@ "expo-web-browser": "~14.2.0", "nativewind": "^4.1.23", "openapi-fetch": "^0.14.0", + "openapi-react-query": "^0.5.0", "react": "19.0.0", "react-dom": "19.0.0", "react-native": "0.79.5", "react-native-css-interop": "^0.1.22", "react-native-gesture-handler": "~2.24.0", + "react-native-maps": "1.20.1", "react-native-reanimated": "~3.17.4", - "react-native-safe-area-context": "^5.5.2", + "react-native-safe-area-context": "5.4.0", "react-native-screens": "~4.11.1", - "react-native-svg": "^15.2.0", + "react-native-svg": "15.11.2", "react-native-web": "~0.20.0", "react-native-webview": "13.13.5", "tailwind-variants": "^0.1.20", @@ -54,6 +57,7 @@ "@ls-lint/ls-lint": "^2.3.1", "@stoplight/prism-cli": "^5.14.2", "@types/react": "~19.0.10", + "@types/react-native-maps": "^0.24.1", "eslint": "^9.25.0", "eslint-config-expo": "~9.2.0", "eslint-config-prettier": "^10.1.5", @@ -3589,6 +3593,20 @@ "integrity": "sha512-nGXMNMclZgzLUxijQQ38Dm3IAEhgxuySAWQHnljFtfB0JdaMwpe0Ox9H7Tp2OgrEA+EMEv+Od9ElKlHwGKmmvQ==", "license": "MIT" }, + "node_modules/@react-native/virtualized-lists": { + "version": "0.72.8", + "resolved": "https://registry.npmjs.org/@react-native/virtualized-lists/-/virtualized-lists-0.72.8.tgz", + "integrity": "sha512-J3Q4Bkuo99k7mu+jPS9gSUSgq+lLRSI/+ahXNwV92XgJ/8UgOTxu2LPwhJnBk/sQKxq7E8WkZBnBiozukQMqrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "invariant": "^2.2.4", + "nullthrows": "^1.1.1" + }, + "peerDependencies": { + "react-native": "*" + } + }, "node_modules/@react-navigation/bottom-tabs": { "version": "7.4.2", "resolved": "https://registry.npmjs.org/@react-navigation/bottom-tabs/-/bottom-tabs-7.4.2.tgz", @@ -4327,6 +4345,32 @@ "tslib": "^2.8.0" } }, + "node_modules/@tanstack/query-core": { + "version": "5.83.0", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.83.0.tgz", + "integrity": "sha512-0M8dA+amXUkyz5cVUm/B+zSk3xkQAcuXuz5/Q/LveT4ots2rBpPTZOzd7yJa2Utsf8D2Upl5KyjhHRY+9lB/XA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.83.0", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.83.0.tgz", + "integrity": "sha512-/XGYhZ3foc5H0VM2jLSD/NyBRIOK4q9kfeml4+0x2DlL6xVuAcVEW+hTlTapAmejObg0i3eNqhkr2dT+eciwoQ==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.83.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, "node_modules/@tootallnate/once": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", @@ -4396,6 +4440,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "license": "MIT" + }, "node_modules/@types/graceful-fs": { "version": "4.1.9", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", @@ -4467,6 +4517,28 @@ "csstype": "^3.0.2" } }, + "node_modules/@types/react-native": { + "version": "0.72.8", + "resolved": "https://registry.npmjs.org/@types/react-native/-/react-native-0.72.8.tgz", + "integrity": "sha512-St6xA7+EoHN5mEYfdWnfYt0e8u6k2FR0P9s2arYgakQGFgU1f9FlPrIEcj0X24pLCF5c5i3WVuLCUdiCYHmOoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@react-native/virtualized-lists": "^0.72.4", + "@types/react": "*" + } + }, + "node_modules/@types/react-native-maps": { + "version": "0.24.1", + "resolved": "https://registry.npmjs.org/@types/react-native-maps/-/react-native-maps-0.24.1.tgz", + "integrity": "sha512-5yAoJOnjwVRyK8iGGxN7d1ZNbxptHFeEgZKv1iZSAE76lgP22x5uXL3CV9JuCgzp3pvRQMC35xFk/9i5McfItA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/react": "*", + "@types/react-native": "*" + } + }, "node_modules/@types/stack-utils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", @@ -11998,6 +12070,19 @@ "openapi-typescript-helpers": "^0.0.15" } }, + "node_modules/openapi-react-query": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/openapi-react-query/-/openapi-react-query-0.5.0.tgz", + "integrity": "sha512-VtyqiamsbWsdSWtXmj/fAR+m9nNxztsof6h8ZIsjRj8c8UR/x9AIwHwd60IqwgymmFwo7qfSJQ1ZzMJrtqjQVg==", + "license": "MIT", + "dependencies": { + "openapi-typescript-helpers": "^0.0.15" + }, + "peerDependencies": { + "@tanstack/react-query": "^5.25.0", + "openapi-fetch": "^0.14.0" + } + }, "node_modules/openapi-typescript": { "version": "7.8.0", "resolved": "https://registry.npmjs.org/openapi-typescript/-/openapi-typescript-7.8.0.tgz", @@ -13537,6 +13622,28 @@ "react-native": "*" } }, + "node_modules/react-native-maps": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/react-native-maps/-/react-native-maps-1.20.1.tgz", + "integrity": "sha512-NZI3B5Z6kxAb8gzb2Wxzu/+P2SlFIg1waHGIpQmazDSCRkNoHNY4g96g+xS0QPSaG/9xRBbDNnd2f2/OW6t6LQ==", + "license": "MIT", + "dependencies": { + "@types/geojson": "^7946.0.13" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": ">= 17.0.1", + "react-native": ">= 0.64.3", + "react-native-web": ">= 0.11" + }, + "peerDependenciesMeta": { + "react-native-web": { + "optional": true + } + } + }, "node_modules/react-native-reanimated": { "version": "3.17.5", "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-3.17.5.tgz", @@ -13573,9 +13680,9 @@ } }, "node_modules/react-native-safe-area-context": { - "version": "5.5.2", - "resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.5.2.tgz", - "integrity": "sha512-t4YVbHa9uAGf+pHMabGrb0uHrD5ogAusSu842oikJ3YKXcYp6iB4PTGl0EZNkUIR3pCnw/CXKn42OCfhsS0JIw==", + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.4.0.tgz", + "integrity": "sha512-JaEThVyJcLhA+vU0NU8bZ0a1ih6GiF4faZ+ArZLqpYbL6j7R3caRqj+mE3lEtKCuHgwjLg3bCxLL1GPUJZVqUA==", "license": "MIT", "peerDependencies": { "react": "*", @@ -13598,13 +13705,14 @@ } }, "node_modules/react-native-svg": { - "version": "15.2.0", - "resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-15.2.0.tgz", - "integrity": "sha512-R0E6IhcJfVLsL0lRmnUSm72QO+mTqcAOM5Jb8FVGxJqX3NfJMlMP0YyvcajZiaRR8CqQUpEoqrY25eyZb006kw==", + "version": "15.11.2", + "resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-15.11.2.tgz", + "integrity": "sha512-+YfF72IbWQUKzCIydlijV1fLuBsQNGMT6Da2kFlo1sh+LE3BIm/2Q7AR1zAAR6L0BFLi1WaQPLfFUC9bNZpOmw==", "license": "MIT", "dependencies": { "css-select": "^5.1.0", - "css-tree": "^1.1.3" + "css-tree": "^1.1.3", + "warn-once": "0.1.1" }, "peerDependencies": { "react": "*", diff --git a/mobile-app/package.json b/mobile-app/package.json index add2076..0fa165e 100644 --- a/mobile-app/package.json +++ b/mobile-app/package.json @@ -12,7 +12,9 @@ "ios": "DARK_MODE=media expo start --ios", "web": "DARK_MODE=media expo start --web", "mock:example": "prism mock ../openapi-specifications/example.swagger.json --port 3001", + "mock": "prism mock ../openapi-specifications/api.swagger.json --port 3001", "gen-schema:example": "openapi-typescript ../openapi-specifications/example.swagger.json -o ./schema/example.d.ts", + "gen-schema": "openapi-typescript ../openapi-specifications/api.swagger.json -o ./schema/api.d.ts", "lint": "expo lint", "lint:fix": "expo lint --fix", "format": "prettier --write .", @@ -32,6 +34,7 @@ "@react-navigation/bottom-tabs": "^7.3.10", "@react-navigation/elements": "^2.3.8", "@react-navigation/native": "^7.1.6", + "@tanstack/react-query": "^5.83.0", "babel-plugin-module-resolver": "^5.0.2", "expo": "~53.0.17", "expo-blur": "~14.1.5", @@ -48,15 +51,17 @@ "expo-web-browser": "~14.2.0", "nativewind": "^4.1.23", "openapi-fetch": "^0.14.0", + "openapi-react-query": "^0.5.0", "react": "19.0.0", "react-dom": "19.0.0", "react-native": "0.79.5", "react-native-css-interop": "^0.1.22", "react-native-gesture-handler": "~2.24.0", + "react-native-maps": "1.20.1", "react-native-reanimated": "~3.17.4", - "react-native-safe-area-context": "^5.5.2", + "react-native-safe-area-context": "5.4.0", "react-native-screens": "~4.11.1", - "react-native-svg": "^15.2.0", + "react-native-svg": "15.11.2", "react-native-web": "~0.20.0", "react-native-webview": "13.13.5", "tailwind-variants": "^0.1.20", @@ -67,6 +72,7 @@ "@ls-lint/ls-lint": "^2.3.1", "@stoplight/prism-cli": "^5.14.2", "@types/react": "~19.0.10", + "@types/react-native-maps": "^0.24.1", "eslint": "^9.25.0", "eslint-config-expo": "~9.2.0", "eslint-config-prettier": "^10.1.5", diff --git a/openapi-specifications/api.swagger.json b/openapi-specifications/api.swagger.json new file mode 100644 index 0000000..f0f02d0 --- /dev/null +++ b/openapi-specifications/api.swagger.json @@ -0,0 +1,238 @@ +{ + "openapi": "3.0.3", + "info": { + "description": "This is a template API server using Go Gin framework", + "title": "Template Mobile App API", + "contact": {}, + "version": "1.0" + }, + "servers": [ + { + "url": "http://localhost:8080/api/v1", + "description": "Development server" + } + ], + "paths": { + "/health": { + "get": { + "description": "Returns the health status of the API", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "additionalProperties": true, + "type": "object" + } + } + }, + "description": "OK" + } + }, + "summary": "Health check endpoint", + "tags": [ + "health" + ] + } + }, + "/posts": { + "get": { + "description": "Fetch all posts from JSONPlaceholder API and return formatted response", + "responses": { + "200": { + "content": { + "application/json": { + "examples": { + "success": { + "value": [ + { + "body": "Sample post body content", + "formattedAt": "2024-01-01T12:00:00Z", + "id": 1, + "title": "Sample Post Title", + "userId": 1 + }, + { + "body": "Another post body content", + "formattedAt": "2024-01-01T12:30:00Z", + "id": 2, + "title": "Another Post Title", + "userId": 2 + } + ] + } + }, + "schema": { + "items": { + "$ref": "#/components/schemas/PostResponse" + }, + "type": "array" + } + } + }, + "description": "OK" + }, + "500": { + "content": { + "application/json": { + "examples": { + "error": { + "value": { + "error": "Internal server error", + "message": "Failed to fetch data" + } + } + }, + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "summary": "Get all posts from JSONPlaceholder", + "tags": [ + "posts" + ] + } + }, + "/posts/{id}": { + "get": { + "description": "Fetch a specific post by ID from JSONPlaceholder API and return formatted response", + "parameters": [ + { + "description": "Post ID", + "in": "path", + "name": "id", + "required": true, + "type": "integer" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "examples": { + "success": { + "value": { + "body": "Sample post body content", + "formattedAt": "2024-01-01T12:00:00Z", + "id": 1, + "title": "Sample Post Title", + "userId": 1 + } + } + }, + "schema": { + "$ref": "#/components/schemas/PostResponse" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "examples": { + "error": { + "value": { + "error": "Internal server error", + "message": "Failed to fetch data" + } + } + }, + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "404": { + "content": { + "application/json": { + "examples": { + "error": { + "value": { + "error": "Internal server error", + "message": "Failed to fetch data" + } + } + }, + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "500": { + "content": { + "application/json": { + "examples": { + "error": { + "value": { + "error": "Internal server error", + "message": "Failed to fetch data" + } + } + }, + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "summary": "Get a post by ID from JSONPlaceholder", + "tags": [ + "posts" + ] + } + } + }, + "components": { + "schemas": { + "ErrorResponse": { + "properties": { + "error": { + "example": "Internal server error", + "type": "string" + }, + "message": { + "example": "Failed to fetch data", + "type": "string" + } + }, + "type": "object" + }, + "PostResponse": { + "properties": { + "body": { + "example": "Sample post body content", + "type": "string" + }, + "formattedAt": { + "example": "2024-01-01T12:00:00Z", + "type": "string" + }, + "id": { + "example": 1, + "type": "integer" + }, + "title": { + "example": "Sample Post Title", + "type": "string" + }, + "userId": { + "example": 1, + "type": "integer" + } + }, + "type": "object" + } + } + } +} \ No newline at end of file