ããã¸ã¹ããã¼ã ããã°ãªã¬ã¼6æ¥ç®ã
ããã«ã¡ã¯ããã¸ã¹ããã¼ã ã«æå±ãã¦ãã大åã§ãã
ã¿ã¤ãã«ã«å«ã¾ããOpenAPIã¨ããæåãããChatGPTãGPT-4ã§è©±é¡ã®OpenAIã¨è¦ééããæ¹ãããã£ãããããããã¾ããããä»åã¯REST APIã®ã¹ãã¼ãå®ç¾©ã«ä½¿ç¨ãããOpenAPIã®è©±ããã¾ãã
ç§ãæå±ãããã¸ã¹ããã¼ã ã§ã¯ãã¸ã¹ã診çã¨ãããµã¼ãã¹ãéçºãã¦ããããã¤ã¯ããµã¼ãã¹ã¢ã¼ããã¯ãã£ã§OpenAPIãå©ç¨ãã¦éçºãã¦ãã¾ãã
ãªããã·ã¹ãã ã®ã¢ã¼ããã¯ãã£ã«ã¤ãã¦ã¯ä»¥ä¸ã®è¨äºã詳ããã§ãã
ä»åã¯ã©ã®ãããªæ§æã§OpenAPIãå©ç¨ããã¹ãã¼ãé§åéçºãè¡ã£ã¦ãããã«ã¤ãã¦ä¾ãç¨ãã¦ç´¹ä»ãã¾ãã
- ããã
- ã¹ãã¼ãå®ç¾©ãã¡ã¤ã«ã®æ§æ
- ã¯ã©ã¤ã¢ã³ãã³ã¼ãçæ
- Swagger UIã®æ´»ç¨
- We are hiring!
ããã
ãã¸ã¹ã診çã§ã¯ãOpenAPIã«ããã¹ãã¼ãå®ç¾©ãéä¸ãã¦ç®¡çãããªãã¸ããªãæºåãã¦ãã¾ãã
ãã®æ§æã«ãã¦ããã®ã¯æ¬¡ã®çç±ããã§ãã
- REST APIã®ã¿ã§ãªããã¤ã¯ããµã¼ãã¹éã®éä¿¡ããã¹ã¦å®ç¾©ãã¦åå®å
¨ã«ãã
- ã¢ããª/Webããã³ããå©ç¨ããGateway API
- ã¤ã³ã¿ã¼ãã«ãªãã¤ã¯ããµã¼ãã¹API
- ã¤ãã³ãé§åã«ããããåã¤ãã³ãã®JSONå½¢å¼
- WebSocketéä¿¡æã®payloadã®JSONå½¢å¼
- å¤æ´ãã²ã¨ã¤ã®Pull Requestã§ç¢ºèªã§ããããã«ããããã«ããµã¼ãã¹å ¨ä½ã®APIå®ç¾©ãã¢ãã¬ãã§ç®¡çãã
- APIå®ç¾©ãæ°è»½ã«ç¢ºèªã§ããGUIãæä¾ãã (Swagger UI)
- Pull Requestã®ã¬ãã¥ã¼æã«ãæ´»ç¨ãã¦ãã
- çæããã¯ã©ã¤ã¢ã³ãã©ã¤ãã©ãªã社å maven/npmã¬ãã¸ããªã§ãã¼ã¸ã§ã³ç®¡çãã
次ããå®éã®ã³ã¼ããä¾ã«èª¬æãã¦ããã¾ãã
ã¹ãã¼ãå®ç¾©ãã¡ã¤ã«ã®æ§æ
ãªãã¸ããªä¸ã®æ§æãä¸é¨çç¥ãã¦ä»¥ä¸ã«ç¤ºãã¾ãã
- client/
- js/
- kotlin/
- internal/
- shard/
- event/
- .openapi-generator-ignore
- build.gradle.kts
- openapi-generator-config.yml
- event/
- visit-api/
- .openapi-generator-ignore
- build.gradle.kts
- openapi-generator-config.yml
- shard/
- build.gradle.kts
- settings.gradle
- version.gradle.kts
- internal/
- components/
- base/
- GenericError404Response.yml
- Visit.yml
- events/
- patient-api/
- visit-api/
- VisitDetail.yml
- base/
- internal/
- visit-api/
- v1_visits.yml
- v1_visits_visitId.yml
- visit-api-openapi.yml
- visit-api/
- public/
- patient-api/
- patient-api-openapi.yml
- shared/
- event-openapi.yml
- template/
- index.html
- Makefile
åãã£ã¬ã¯ããªã«ä½ãä¿åããã¦ããã®ãã«ã¤ãã¦æ¬¡ãã説æãã¦ããã¾ãã
APIãµã¼ãã®å®ç¾©
åAPIãµã¼ãã«å¯¾å¿ããå®ç¾©ã¯ä»¥ä¸ã®å ´æã«ç½®ããã¾ãã
internal/*-openapi.yml
... k8sã¯ã©ã¹ã¿å ã«éãã¦ããAPIãµã¼ãã®å®ç¾©public/*-openapi.yml
... å¤ããã¢ã¯ã»ã¹å¯è½ãªAPIãµã¼ãã®å®ç¾©
ä¸ä¾ã¨ã㦠internal/visit-api-openapi.yml
ã®å®ç¾©ãæç²ãã¾ãã
openapi: 3.0.3 info: title: visit-api description: å é¨åãåä»API version: "1.0" servers: - url: http://visit-api.default.svc.cluster.local paths: /v1/visits: $ref: "./visit-api/v1_visits.yml" /v1/visits/{visitId}: $ref: "./visit-api/v1_visits_visitId.yml"
APIã®åendpointã«å¯¾ãã¦ã²ã¨ã¤ãã¤ãã¡ã¤ã«ãä½æããå½¢ã«ãªã£ã¦ãããHTTP request methodããã³ãã®å®ç¾©ãè¨è¿°ããå½¢ã«ãªã£ã¦ãã¾ãã
ãã®ãã¡ã¤ã«ä¸ã§ã¯ components/
以ä¸ã«ç½®ããã¦ãããå
±éåãããå®ç¾©ãã¡ã¤ã«ãèªã¿è¾¼ãã§ä½¿ç¨ãã¦ãã¾ãã
get: tags: - visits summary: å診詳細 operationId: getVisit parameters: - in: path name: visitId required: true schema: type: string format: uuid responses: "200": description: å診ä¸è¦§ content: application/json: schema: $ref: "../../components/visit-api/VisitDetail.yml" "404": $ref: "../../components/base/GenericError404Response.yml" put: tags: - visits summary: å診æ´æ° : patch: tags: - visits summary: Visitç·¨é :
åAPIã®ãªãã¸ããªã¯ããããå¥ã«ç¨æããã¦ããããã®ä¸ã§ãã®ã¹ãã¼ãå®ç¾©ãåç §ãã¦ããããçæããã¾ãã
å ±éåããããã¡ã¤ã«ã®å®ç¾©
åAPIã§å
±æãã¦ä½¿ç¨ãããå®ç¾©ãã¡ã¤ã«ã¯ compoents/
以ä¸ã«é
ç½®ããã¾ãã
components/base/*.yml
... å ±æãããã¹ãã¼ãå®ç¾©components/*-api/*yml
... åAPIæ¯ã®ã¹ãã¼ãã®éããå¸åããå®ç¾©
2ã¤ã®éããããã ãã§ã¯ããããªãã®ã§ãå ·ä½ä¾ãæãã¾ãã
base以ä¸ã«é
ç½®ããããã®ã¯ããã®ã³ã³ãã¼ãã³ããæä½éæã¤ã¹ããã®ãå®ç¾©ããã¦ãã¾ãã
以ä¸ã®visit (æ½è¨ã¸ã®è¨ªå) ã§ã¯ãIDãæ¥ä»ãç¾å¨ã®ç¶æ
ãããã³æä½ãããæ¥ä»ã®æ
å ±ãæã£ã¦ãã¾ãã
components/base/Visit.yml
type: object required: - id - state - date - createdAt description: å診(ãã§ãã¯ã¤ã³, prescriptionTypeã¯ä½ãå¸æãã¦ããªããªãnull) properties: id: type: string format: uuid state: $ref: "./VisitState.yml" date: type: string format: date createdAt: type: string format: date-time checkedInAt: type: string format: date-time cancelledAt: type: string format: date-time
ä¸æ¹ã§åAPIã§ä½¿ç¨ããããã®ã¤ãã¦ã¯ãåAPIã®ç®çã«å¿ãã¦å¤ã追å ããã¾ãã
以ä¸ã®ä¾ã§ã¯ç´ä»ããããè¬å±ã®IDã追å ã§è¿ãã¾ãã
components/visit-api/VisitDetail.yml
allOf: - $ref: "./Visit.yml" - type: object properties: pharmacyId: type: string format: uuid
ãã®æ§æã¯ãAPIãµã¼ãããã³APIã¯ã©ã¤ã¢ã³ãã§ã®çæç¶æ³ãè¦ã¦ãæªããååã®ãã¡ã¤ã«ãçæãããªããã¨ã確èªããªããé ç½®ãã¦ãã¾ãã
ã¤ãã³ãé§åç¨ã®å®ç¾©
ã¤ãã³ãé§åç¨ã®ã¹ãã¼ãå®ç¾©ã¯ä»¥ä¸ã®å ´æã«ç½®ããã¦ãã¾ãã
components/events/*.yml
shared/event-openapi.yml
components/events/
以ä¸ã®ãã¡ã¤ã«ã¯ä»ã®ã³ã³ãã¼ãã³ãã¨åæ§ã« type: object
ãªã©ã«ããå®ç¾©ããã¦ãã¾ãã
工夫ãã¦ããç¹ã¨ãã¦ãOpenAPIã®ä½¿ç¨æ³ããããé¸è±ãã¾ãããããã¼ã®endpointãå®ç¾©ãã¦ä½¿ç¨ããã¹ãã¼ãå®ç¾©ãèªã¿è¾¼ããã¨ã«ãããã¤ãã³ãé§åç¨ã¹ãã¼ãã«ã¤ãã¦ãã³ã¼ããçæãã¦ãã¾ãã
ããã«å½ããã®ã shared/event-open-api.yml
ã§ããã以ä¸ã®æ§ã«ãªã£ã¦ãã¾ãã
openapi: 3.0.3 info: title: events description: ã¤ãã³ãã®schema version: "1.0" paths: /dummy: get: responses: "200": description: "success" content: application/json: schema: type: object properties: WebsocketPayload: $ref: "../components/events/WebsocketPayload.yml" Event: $ref: "../components/events/Event.yml" PushData: $ref: "../components/events/PushData.yml"
ããã«ããSwagger UIçãããã¹ãã¼ãå®ç¾©ã確èªã§ããããã«ãªãã¾ãã
ã¤ãã³ã以å¤ã«ãWebSocketã®payloadãããã·ã¥éç¥ã®payloadã«ã¤ãã¦ãåæ§ã«ç®¡çãã¦ãã¾ãã
ã¯ã©ã¤ã¢ã³ãã³ã¼ãçæ
ãã®ãªãã¸ããªã§ã¯Kotlinããã³JavaScriptç¨ã®ã¯ã©ã¤ã¢ã³ãã©ã¤ãã©ãªã®çæãæ
ã£ã¦ãã¾ãã
å¼ç¤¾ã§ã¯Mavenãnpmçã®ç¤¾å
ãªãã¸ããªãæ´»ç¨ãã¦ããããã®ãªãã¸ããªããã¢ãããã¼ããããã¨ã§åAPIãã使ããããã«ãã¦ãã¾ãã
ä¸ä¾ã¨ãã¦Kotlinç¨ã®ã©ã¤ãã©ãªãã©ã®ããã«çæãã¦ããã®ãã«ã¤ãã¦ç´¹ä»ãã¾ãã
åºæ¬çã«ã¯openapi-generator-cliã³ãã³ããå®è¡ãã¦ããã ãã§ããã®é¨åã¯Makefileã§ç®¡çãã¦ãã¾ã *1ã
.PHONY: help generate-and-publish-kotlin-client generate-and-publish-kotlin-clients .DEFAULT_GOAL := help help: @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' generate-and-publish-kotlin-client: ## publish client docker run --rm -v $$(pwd):/local openapitools/openapi-generator-cli:v5.1.1 generate -i /local/${TARGET}-openapi.yml -g kotlin -o /local/client/kotlin/${TARGET} -c /local/client/kotlin/${TARGET}/openapi-generator-config.yml && (cd client/kotlin/${TARGET} && ./gradlew clean publish) generate-and-publish-kotlin-clients: ## publish all client find -E ./client/kotlin -maxdepth 2 -mindepth 2 -regex '.*(internal|shared).*' \ | cut -sd / -f 4- \ | xargs -P 50 -I{} make generate-and-publish-kotlin-client TARGET={} \ && (cd client/kotlin && ./gradlew publish)
client/kotlin/
以ä¸ã«ãã openapi-generator-config.yml
ãèªã¿è¾¼ãã§çæãã¾ãã
ä¾. client/kotlin/internal/visit-api/openapi-generator-config.yml
groupId: com.example artifactId: visit-api-client packageName: com.example.visitapi.client enumPropertyNaming: PascalCase serializationLibrary: jackson
client/kotlin/
以ä¸ã«ã¯å
±éãã¦ä½¿ç¨ããããã¡ã¤ã«ãè¨ç½®ããã¦ãã¾ãã
version.gradle.kts
import java.io.ByteArrayOutputStream version = buildVersion("1.0.0") // https://discuss.gradle.org/t/how-to-run-execute-string-as-a-shell-command-in-kotlin-dsl/32235/10 fun getGitBranchName(): String { val out = ByteArrayOutputStream() project.exec { commandLine = listOf("git", "rev-parse", "--abbrev-ref", "HEAD") standardOutput = out } return String(out.toByteArray()).trim() } fun buildVersion(version: String): String { return if (project.hasProperty("release")) { version } else { val branch = getGitBranchName().replace(Regex("[/_]"), "-") "$version-$branch-SNAPSHOT" } }
client/kotliin/build.gradle.kts
buildscript { repositories { maven { url = uri("https://repo1.maven.org/maven2") } maven("https://plugins.gradle.org/m2/") } } group = "com.example" apply { from("./version.gradle.kts") } repositories { mavenCentral() maven { url = uri("https://repo1.maven.org/maven2") } } plugins { `java-platform` `maven-publish` } dependencies { constraints { api("com.example:event-schema:${project.version}") api("com.example:visit-api-client:${project.version}") } javaPlatform { allowDependencies() } tasks { getByName("publish") { doLast { logger.lifecycle("published version: \n$version") } } }
ãã®ä¸ã§å
±éã®versionã管çãã¦BOMãçæãããã¨ã§ãè¤æ°ã®ã¯ã©ã¤ã¢ã³ãã©ã¤ãã©ãªãã¾ã¨ãã¦æ´æ°ã§ããããã«ãã¦ãã¾ãã
ã¾ãå½åã¯ä¸¦åã§éçºããéã«åãversionãä¸æ¸ããã¦ãã¾ãäºæ
ãçºçãã¦ãããããversionå
ã«Gitã®ãã©ã³ãåãå
¥ãããã¨ã§è¡çªããªãããã«ãã¦ãã¾ãã
Swagger UIã®æ´»ç¨
æå¾ã«ãAPIå®ç¾©ãã¾ã¨ãã¦ç¢ºèªã§ããããã¥ã¡ã³ãçæã«ã¤ãã¦ç´¹ä»ãã¾ãã
ãã¡ããMakefileã§ç®¡çãã¦ããã®ã§ãååã®Makefileã«è¿½å ããé¨åã以ä¸ã«ç¤ºãã¾ãã
TARGET_YAMLS := find . -maxdepth 2 -mindepth 2 -regextype posix-egrep -regex '.*(internal|public|shared).*-openapi\.yml' | sed -e 's|^\./||' | sed 's/-openapi.yml//' SWAGGER_UI_VALUES := $(TARGET_YAMLS) | xargs -n1 echo | xargs -I{} echo '{name:"{}",url:"./{}/openapi.json"}' publish-documents: ## publish documents @$(TARGET_YAMLS) | xargs -n1 echo | xargs -I{} mkdir -p publish/{} @$(TARGET_YAMLS) | xargs -n1 echo | xargs -I{} -P10 openapi-generator-cli generate -i {}-openapi.yml -g openapi -o publish/{} @SWAGGER_UI_VALUES=$$($(SWAGGER_UI_VALUES) | tr '\n' ',' | sed 's/,$$//'); cat template/index.html | sed "s|\$$SWAGGER_UI_VALUES|[$$SWAGGER_UI_VALUES]|" > publish/index.html
ããã§ã¯swagger-uiãå©ç¨ã§ããããã«ããHTMLãã¡ã¤ã«ã«ãJSONå½¢å¼ã§çæããã¹ãã¼ããã¡ã¤ã«ã®ãã¹ãè¨å®ãã¦ãã¾ãã
å
·ä½çã«ã¯ template/index.html
ã® $SWAGGER_UI_VALUES
ã [{name:"internal/visit-api",url:"./internal/visit-api/openapi.json"},{name:"shared/event",url:"./shared/event/openapi.json"}]
ãªã©ã«ç½®ãæãã¦ãã¾ãã
template/index.html
<!DOCTYPE html> <html> <head> <title>API spec</title> <meta charset="utf-8" /> <link rel="stylesheet" href="https://unpkg.com/[email protected]/swagger-ui.css"> <script src="https://unpkg.com/[email protected]/swagger-ui-bundle.js"></script> <script src="https://unpkg.com/[email protected]/swagger-ui-standalone-preset.js"></script> <script> window.onload = () => { window.ui = SwaggerUIBundle({ urls: $SWAGGER_UI_VALUES, dom_id: '#swagger-ui', presets: [ SwaggerUIBundle.presets.apis, SwaggerUIStandalonePreset, ], layout: "StandaloneLayout", }); }; </script> </head> <body> <div id="swagger-ui"></div> </body> </html>
make publish-documents
ãå®è¡ãããã¨ã§ publish/
ã«ä»¥ä¸ã®ãã¡ã¤ã«ãçæãããããããHTTPãµã¼ãä¸ã«é
ç½®ãããã¨ã§Swagger UIãå©ç¨ã§ããããã«ãªãã¾ãã
- index.html
- internal/
- visit-api/
- README.md
- openapi.json
- visit-api/
- public/
- patient-api/
- README.md
- openapi.json
- patient-api/
- shared/
- event/
- README.md
- openapi.json
- event/
ãã®HTTPãµã¼ãã«ã¢ãããã¼ãããå¦çãCI/CDã§è¡ããã¨ã§ãå ã«ã¹ãã¼ãå®ç¾©ãã¡ã¤ã«ã ãã§Pull Requestãä½æãã¦ãSwagger UIä¸ã§ç¢ºèªããªããã³ã¼ãã¬ãã¥ã¼ã§ãã¾ãã
ã¾ã¨ã
å¼ãã¼ã ã«ããã¦OpenAPIã«ããã¹ãã¼ãå®ç¾©ãéä¸ç®¡çãã¦ãããªãã¸ããªã«ã¤ãã¦ç´¹ä»ãã¾ããã
ãã®ä»çµã¿ãæ´åãããã¨ã«ãããå
ã«ã¹ãã¼ããè°è«ãã¦ããéçºããã¹ã¿ã¤ã«ã§ã¹ã ã¼ãºã«éçºã§ãã¦ãã¾ãã
ç§ã以åè¡ã£ã¦ãã¾ããããAPIãµã¼ãã¨ã¯ã©ã¤ã¢ã³ãã©ã¤ãã©ãªãåæã«ã¡ã³ããã³ã¹ããã®ã¯å¤§å¤ãªã®ã§ãOpenAPIãæ´»ç¨ãã¦æ¥½ããã¦ããã¾ãããã
åè
ä»åã¯OpenAPIèªä½ã«ã¤ãã¦ã¯èª¬æãã¾ããã§ããããããä¸æç¹ãããã°ä»¥ä¸ã®éå»è¨äºãåèã«ãã¦ããã ããã¨å¹¸ãã§ãã
We are hiring!
ã¹ãã¼ãé§åéçºãããããå§ãã¦ããããæ¹ãããªããªè¡ã£ã¦ããæ¹ãç¹ã«ç´¹ä»ããæ¹æ³ããã£ã¨æ¹åã§ããã¨ããæ¹ã¯ä¸åº¦ã話ãã¦ã¿ã¾ãããï¼
ããèå³ãããã°ä»¥ä¸ãããé£çµ¡ããã ããã¨ããããã§ãï¼
*1:helpã«ã¤ãã¦ã¯æ¬¡ã®è¨äºãåç §: https://postd.cc/auto-documented-makefile/