From dd4bb73a78d82537c6a1b27ee141e445d4b68a48 Mon Sep 17 00:00:00 2001 From: timurgordon Date: Mon, 10 Feb 2025 12:39:19 +0300 Subject: [PATCH] fix & improve actor code generation --- .../openapi_e2e/generate_actor_module.vsh | 1 + .../baobab/generator/openapi_e2e/openapi.json | 493 ++++++++++-------- lib/baobab/generator/generate_actor.v | 2 + lib/baobab/generator/generate_scripts.v | 1 + .../generator/templates/run.sh.template | 4 +- lib/core/code/module.v | 16 +- lib/core/code/templates/v.mod.template | 7 + lib/core/texttools/casing.v | 4 + lib/schemas/openapi/decode.v | 3 + lib/schemas/openapi/decode_test.v | 6 +- lib/schemas/openapi/testdata/openapi.json | 14 +- 11 files changed, 328 insertions(+), 223 deletions(-) create mode 100644 lib/core/code/templates/v.mod.template diff --git a/examples/baobab/generator/openapi_e2e/generate_actor_module.vsh b/examples/baobab/generator/openapi_e2e/generate_actor_module.vsh index 66c0cb63..90b1975c 100755 --- a/examples/baobab/generator/openapi_e2e/generate_actor_module.vsh +++ b/examples/baobab/generator/openapi_e2e/generate_actor_module.vsh @@ -20,6 +20,7 @@ actor_module := generator.generate_actor_module( actor_module.write(example_dir, format: true overwrite: true + compile: true )! os.execvp('bash', ['${example_dir}/meeting_scheduler_actor/scripts/run.sh'])! \ No newline at end of file diff --git a/examples/baobab/generator/openapi_e2e/openapi.json b/examples/baobab/generator/openapi_e2e/openapi.json index c3967cb0..bea4a619 100644 --- a/examples/baobab/generator/openapi_e2e/openapi.json +++ b/examples/baobab/generator/openapi_e2e/openapi.json @@ -1,163 +1,127 @@ { - "openapi": "3.0.0", - "info": { - "title": "Meeting Scheduler", - "version": "1.0.0", - "description": "An API for managing meetings, availability, and scheduling." + "openapi": "3.0.0", + "info": { + "title": "Meeting Scheduler", + "version": "1.0.0", + "description": "An API for managing meetings, availability, and scheduling." + }, + "servers": [ + { + "url": "http://localhost:8080/openapi/v1", + "description": "Production server" }, - "servers": [ - { - "url": "http://localhost:8080/openapi/v1", - "description": "Production server" - }, - { - "url": "http://localhost:8081/openapi/v1", - "description": "Example server" - } - ], - "paths": { - "/users": { - "get": { - "summary": "List all users", - "responses": { - "200": { - "description": "A list of users", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/User" - } - } - } - } - } - } - } - }, - "/users/{userId}": { - "get": { - "operationId": "get_user", - "summary": "Get user by ID", - "parameters": [ - { - "name": "userId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "uint32" - }, - "description": "The ID of the user" - } - ], - "responses": { - "200": { - "description": "User details", - "content": { - "application/json": { - "schema": { + { + "url": "http://localhost:8081/openapi/v1", + "description": "Example server" + } + ], + "paths": { + "/users": { + "get": { + "summary": "List all users", + "responses": { + "200": { + "description": "A list of users", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { "$ref": "#/components/schemas/User" } - } + }, + "example": [ + { + "id": "1", + "name": "Alice", + "email": "alice@example.com" + }, + { + "id": "2", + "name": "Bob", + "email": "bob@example.com" + } + ] } - }, - "404": { - "description": "User not found" } } } - }, - "/events": { - "post": { - "summary": "Create an event", - "requestBody": { + } + }, + "/users/{userId}": { + "get": { + "operationId": "get_user", + "summary": "Get user by ID", + "parameters": [ + { + "name": "userId", + "in": "path", "required": true, + "schema": { + "type": "integer", + "format": "uint32" + }, + "description": "The ID of the user", + "example": 1 + } + ], + "responses": { + "200": { + "description": "User details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User" + }, + "example": { + "id": "1", + "name": "Alice", + "email": "alice@example.com" + } + } + } + }, + "404": { + "description": "User not found" + } + } + } + }, + "/events": { + "post": { + "summary": "Create an event", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Event" + }, + "example": { + "title": "Team Meeting", + "description": "Weekly sync", + "startTime": "2023-10-10T10:00:00Z", + "endTime": "2023-10-10T11:00:00Z", + "userId": "1" + } + } + } + }, + "responses": { + "201": { + "description": "Event created", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Event" - } - } - } - }, - "responses": { - "201": { - "description": "Event created", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Event" - } - } - } - } - } - } - }, - "/availability": { - "get": { - "summary": "Get availability for a user", - "parameters": [ - { - "name": "userId", - "in": "query", - "required": true, - "schema": { - "type": "string" - }, - "description": "The ID of the user" - }, - { - "name": "date", - "in": "query", - "required": false, - "schema": { - "type": "string", - "format": "date" - }, - "description": "The date to check availability (YYYY-MM-DD)" - } - ], - "responses": { - "200": { - "description": "Availability details", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/TimeSlot" - } - } - } - } - } - } - } - }, - "/bookings": { - "post": { - "summary": "Book a meeting", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Booking" - } - } - } - }, - "responses": { - "201": { - "description": "Booking created", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Booking" - } + }, + "example": { + "id": "101", + "title": "Team Meeting", + "description": "Weekly sync", + "startTime": "2023-10-10T10:00:00Z", + "endTime": "2023-10-10T11:00:00Z", + "userId": "1" } } } @@ -165,82 +129,183 @@ } } }, - "components": { - "schemas": { - "User": { - "type": "object", - "properties": { - "id": { + "/availability": { + "get": { + "summary": "Get availability for a user", + "parameters": [ + { + "name": "userId", + "in": "query", + "required": true, + "schema": { "type": "string" }, - "name": { - "type": "string" - }, - "email": { + "description": "The ID of the user", + "example": "1" + }, + { + "name": "date", + "in": "query", + "required": false, + "schema": { "type": "string", - "format": "email" + "format": "date" + }, + "description": "The date to check availability (YYYY-MM-DD)", + "example": "2023-10-10" + } + ], + "responses": { + "200": { + "description": "Availability details", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TimeSlot" + } + }, + "example": [ + { + "startTime": "10:00:00", + "endTime": "11:00:00", + "available": true + }, + { + "startTime": "11:00:00", + "endTime": "12:00:00", + "available": false + } + ] + } + } + } + } + } + }, + "/bookings": { + "post": { + "summary": "Book a meeting", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Booking" + }, + "example": { + "userId": "1", + "eventId": "101", + "timeSlot": { + "startTime": "10:00:00", + "endTime": "11:00:00", + "available": true + } + } } } }, - "Event": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "title": { - "type": "string" - }, - "description": { - "type": "string" - }, - "startTime": { - "type": "string", - "format": "date-time" - }, - "endTime": { - "type": "string", - "format": "date-time" - }, - "userId": { - "type": "string" - } - } - }, - "TimeSlot": { - "type": "object", - "properties": { - "startTime": { - "type": "string", - "format": "time" - }, - "endTime": { - "type": "string", - "format": "time" - }, - "available": { - "type": "boolean" - } - } - }, - "Booking": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "userId": { - "type": "string" - }, - "eventId": { - "type": "string" - }, - "timeSlot": { - "$ref": "#/components/schemas/TimeSlot" + "responses": { + "201": { + "description": "Booking created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Booking" + }, + "example": { + "id": "5001", + "userId": "1", + "eventId": "101", + "timeSlot": { + "startTime": "10:00:00", + "endTime": "11:00:00", + "available": true + } + } + } } } } } } + }, + "components": { + "schemas": { + "User": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "email": { + "type": "string", + "format": "email" + } + } + }, + "Event": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "startTime": { + "type": "string", + "format": "date-time" + }, + "endTime": { + "type": "string", + "format": "date-time" + }, + "userId": { + "type": "string" + } + } + }, + "TimeSlot": { + "type": "object", + "properties": { + "startTime": { + "type": "string", + "format": "time" + }, + "endTime": { + "type": "string", + "format": "time" + }, + "available": { + "type": "boolean" + } + } + }, + "Booking": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "userId": { + "type": "string" + }, + "eventId": { + "type": "string" + }, + "timeSlot": { + "$ref": "#/components/schemas/TimeSlot" + } + } + } + } } - \ No newline at end of file +} \ No newline at end of file diff --git a/lib/baobab/generator/generate_actor.v b/lib/baobab/generator/generate_actor.v index 9e787f04..2361d39e 100644 --- a/lib/baobab/generator/generate_actor.v +++ b/lib/baobab/generator/generate_actor.v @@ -91,8 +91,10 @@ pub fn generate_actor_module(spec ActorSpecification, params Params) !Module { name_fixed := texttools.snake_case(spec.name) return code.new_module( name: '${name_fixed}_actor' + description: spec.description files: files folders: folders + in_src: true ) } diff --git a/lib/baobab/generator/generate_scripts.v b/lib/baobab/generator/generate_scripts.v index a6996305..b924a634 100644 --- a/lib/baobab/generator/generate_scripts.v +++ b/lib/baobab/generator/generate_scripts.v @@ -22,6 +22,7 @@ pub fn generate_scripts_folder(name string, example bool) Folder { // Function to generate a script for running an actor fn generate_run_script(actor_name string) File { + actor_title := texttools.title_case(actor_name) dollar := '$' return File{ name: 'run' diff --git a/lib/baobab/generator/templates/run.sh.template b/lib/baobab/generator/templates/run.sh.template index 4982ed2b..f90768fb 100755 --- a/lib/baobab/generator/templates/run.sh.template +++ b/lib/baobab/generator/templates/run.sh.template @@ -12,11 +12,11 @@ chmod +x @{dollar}{DIR}/run_http_server.vsh HTTP_SERVER_PID=@{dollar}! # Print desired output -echo "Actor Redis Interface running on redis://localhost:6379" +echo "${actor_title} Actor Redis Interface running on redis://localhost:6379" echo "* /queues/${actor_name} -> Action Interface" echo "" -echo "Actor HTTP Server running on http://localhost:8080" +echo "${actor_title} Actor HTTP Server running on http://localhost:8080" echo "* /playground/openapi -> OpenAPI Playground" echo "* /openapi -> OpenAPI Interface" echo "* /docs -> Documentation" diff --git a/lib/core/code/module.v b/lib/core/code/module.v index 1f8d6776..ef16e04a 100644 --- a/lib/core/code/module.v +++ b/lib/core/code/module.v @@ -7,9 +7,14 @@ import log pub struct Module { pub mut: name string + description string + version string = '0.0.1' + license string = 'apache2' + vcs string = 'git' files []IFile folders []IFolder modules []Module + in_src bool // whether mod will be generated in src folder // model VFile // methods VFile } @@ -27,7 +32,7 @@ pub fn new_module(mod Module) Module { pub fn (mod Module) write(path string, options WriteOptions) ! { mut module_dir := pathlib.get_dir( - path: '${path}/${mod.name}' + path: if mod.in_src { '${path}/${mod.name}/src' } else { '${path}/${mod.name}' } empty: options.overwrite )! @@ -40,11 +45,11 @@ pub fn (mod Module) write(path string, options WriteOptions) ! { } for folder in mod.folders { - folder.write(module_dir.path, options)! + folder.write('${path}/${mod.name}', options)! } for mod_ in mod.modules { - mod_.write(module_dir.path, options)! + mod_.write('${path}/${mod.name}', options)! } if options.format { @@ -61,7 +66,10 @@ pub fn (mod Module) write(path string, options WriteOptions) ! { } } if options.document { - os.execute('v doc -f html -o ${module_dir.path}/docs ${module_dir.path}') + docs_path := '${path}/${mod.name}/docs' + os.execute('v doc -f html -o ${docs_path} ${module_dir.path}') } + mut mod_file := pathlib.get_file(path: '${module_dir.path}/v.mod')! + mod_file.write($tmpl('templates/v.mod.template'))! } diff --git a/lib/core/code/templates/v.mod.template b/lib/core/code/templates/v.mod.template new file mode 100644 index 00000000..b0883a30 --- /dev/null +++ b/lib/core/code/templates/v.mod.template @@ -0,0 +1,7 @@ +Module { + name: '@{mod.name}' + description: '@{mod.description}' + version: '@{mod.version}' + vcs: '@{mod.vcs}' + license: '@{mod.license}' +} diff --git a/lib/core/texttools/casing.v b/lib/core/texttools/casing.v index 0dc00931..123116f8 100644 --- a/lib/core/texttools/casing.v +++ b/lib/core/texttools/casing.v @@ -4,6 +4,10 @@ pub fn snake_case(s string) string { return separate_words(s).join('_') } +pub fn title_case(s string) string { + return separate_words(s).join(' ').title() +} + pub fn pascal_case(s string) string { mut pascal := s.replace('_', ' ') return pascal.title().replace(' ', '') diff --git a/lib/schemas/openapi/decode.v b/lib/schemas/openapi/decode.v index 8476dd34..9bb43c8b 100644 --- a/lib/schemas/openapi/decode.v +++ b/lib/schemas/openapi/decode.v @@ -136,6 +136,9 @@ fn json_decode_content(content_ map[string]MediaType, content_map map[string]Any if schema_any := media_type_map['schema'] { media_type.schema = jsonschema.decode_schemaref(schema_any.as_map())! } + if example_any := media_type_map['example'] { + media_type.example = media_type_map['example'] + } content[key] = media_type } } diff --git a/lib/schemas/openapi/decode_test.v b/lib/schemas/openapi/decode_test.v index 5b7d9a49..9bef2630 100644 --- a/lib/schemas/openapi/decode_test.v +++ b/lib/schemas/openapi/decode_test.v @@ -1,6 +1,7 @@ module openapi import os +import x.json2 {Any} import freeflowuniverse.herolib.schemas.jsonschema {Schema, Reference, SchemaRef} const spec_path = '${os.dir(@FILE)}/testdata/openapi.json' @@ -47,7 +48,8 @@ const spec = openapi.OpenAPI{ 'application/json': openapi.MediaType{ schema: Reference{ ref: '#/components/schemas/Pets' - } + }, + example: Any('[{"id":"1","name":"Alice","email":"alice@example.com"},{"id":"2","name":"Bob","email":"bob@example.com"}]') } } } @@ -387,7 +389,7 @@ fn match_operations(a Operation, b Operation) { assert a.operation_id == b.operation_id, 'Operation ID does not match.' assert a.parameters == b.parameters, 'Parameters do not match.' assert a.request_body == b.request_body, 'Request body does not match.' - assert a.responses == b.responses, 'Responses do not match.' + assert a.responses.str() == b.responses.str(), 'Responses do not match.' assert a.callbacks == b.callbacks, 'Callbacks do not match.' assert a.deprecated == b.deprecated, 'Deprecated flag does not match.' assert a.security == b.security, 'Security requirements do not match.' diff --git a/lib/schemas/openapi/testdata/openapi.json b/lib/schemas/openapi/testdata/openapi.json index c5ab2d9d..7db5ec54 100644 --- a/lib/schemas/openapi/testdata/openapi.json +++ b/lib/schemas/openapi/testdata/openapi.json @@ -39,7 +39,19 @@ "application/json": { "schema": { "$ref": "#/components/schemas/Pets" - } + }, + "example": [ + { + "id": "1", + "name": "Alice", + "email": "alice@example.com" + }, + { + "id": "2", + "name": "Bob", + "email": "bob@example.com" + } + ] } } },