diff --git a/examples/core/code/code_parser.vsh b/examples/core/code/code_parser.vsh new file mode 100755 index 00000000..9ed72015 --- /dev/null +++ b/examples/core/code/code_parser.vsh @@ -0,0 +1,58 @@ +#!/usr/bin/env -S v -n -w -cg -gc none -cc tcc -d use_openssl -enable-globals run + +import incubaid.herolib.core.code +import incubaid.herolib.ui.console +import os + +fn main() { + console.print_header('Code Parser Example - lib/core/pathlib Analysis') + console.print_lf(1) + + pathlib_dir := os.home_dir() + '/code/github/incubaid/herolib/lib/core/pathlib' + + // Step 1: List all V files + console.print_header('1. Listing V Files') + v_files := code.list_v_files(pathlib_dir)! + for file in v_files { + console.print_item(os.base(file)) + } + console.print_lf(1) + + // Step 2: Parse and analyze each file + console.print_header('2. Parsing Files - Summary') + for v_file_path in v_files { + content := os.read_file(v_file_path)! + vfile := code.parse_vfile(content)! + + console.print_item('${os.base(v_file_path)}') + console.print_item(' Module: ${vfile.mod}') + console.print_item(' Imports: ${vfile.imports.len}') + console.print_item(' Structs: ${vfile.structs().len}') + console.print_item(' Functions: ${vfile.functions().len}') + } + console.print_lf(1) + + // Step 3: Find Path struct + console.print_header('3. Analyzing Path Struct') + path_code := code.get_type_from_module(pathlib_dir, 'Path')! + console.print_stdout(path_code) + console.print_lf(1) + + // Step 4: List all public functions + console.print_header('4. Public Functions in pathlib') + for v_file_path in v_files { + content := os.read_file(v_file_path)! + vfile := code.parse_vfile(content)! + + pub_functions := vfile.functions().filter(it.is_pub) + if pub_functions.len > 0 { + console.print_item('From ${os.base(v_file_path)}:') + for f in pub_functions { + console.print_item(' ${f.name}() -> ${f.result.typ.symbol()}') + } + } + } + console.print_lf(1) + + console.print_green('✓ Analysis completed!') +} \ No newline at end of file diff --git a/examples/core/flows/runner_test.vsh b/examples/core/flows/runner_test.vsh new file mode 100755 index 00000000..d5ed81d6 --- /dev/null +++ b/examples/core/flows/runner_test.vsh @@ -0,0 +1,339 @@ +#!/usr/bin/env -S v -n -w -cg -gc none -cc tcc -d use_openssl -enable-globals run + +import incubaid.herolib.core.flows +import incubaid.herolib.core.redisclient +import incubaid.herolib.ui.console +import incubaid.herolib.data.ourtime +import time + +fn main() { + mut cons := console.new() + + console.print_header('Flow Runner Test Suite') + console.print_lf(1) + + // Test 1: Basic Flow Execution + console.print_item('Test 1: Basic Flow with Successful Steps') + test_basic_flow()! + console.print_lf(1) + + // Test 2: Error Handling + console.print_item('Test 2: Error Handling with Error Steps') + test_error_handling()! + console.print_lf(1) + + // Test 3: Multiple Next Steps + console.print_item('Test 3: Multiple Next Steps') + test_multiple_next_steps()! + console.print_lf(1) + + // Test 4: Redis State Retrieval + console.print_item('Test 4: Redis State Retrieval and JSON') + test_redis_state()! + console.print_lf(1) + + // Test 5: Complex Flow Chain + console.print_item('Test 5: Complex Flow Chain') + test_complex_flow()! + console.print_lf(1) + + console.print_header('All Tests Completed Successfully!') +} + +fn test_basic_flow() ! { + mut redis := redisclient.core_get()! + redis.flushdb()! + + mut coordinator := flows.new( + name: 'test_basic_flow', + redis: redis, + ai: none + )! + + // Step 1: Initialize + mut step1 := coordinator.step_new( + name: 'initialize' + description: 'Initialize test environment' + f: fn (mut s flows.Step) ! { + println(' ✓ Step 1: Initializing...') + s.context['init_time'] = ourtime.now().str() + } + )! + + // Step 2: Process + mut step2 := coordinator.step_new( + name: 'process' + description: 'Process data' + f: fn (mut s flows.Step) ! { + println(' ✓ Step 2: Processing...') + s.context['processed'] = 'true' + } + )! + + // Step 3: Finalize + mut step3 := coordinator.step_new( + name: 'finalize' + description: 'Finalize results' + f: fn (mut s flows.Step) ! { + println(' ✓ Step 3: Finalizing...') + s.context['status'] = 'completed' + } + )! + + step1.next_step_add(step2) + step2.next_step_add(step3) + + coordinator.run()! + + // Verify Redis state + state := coordinator.get_all_steps_state()! + assert state.len >= 3, 'Expected at least 3 steps in Redis' + + for step_state in state { + assert step_state['status'] == 'success', 'Expected all steps to be successful' + } + + println(' ✓ Test 1 PASSED: All steps executed successfully') + coordinator.clear_redis()! +} + +fn test_error_handling() ! { + mut redis := redisclient.core_get()! + redis.flushdb()! + + mut coordinator := flows.new( + name: 'test_error_flow', + redis: redis, + ai: none + )! + + // Error step + mut error_recovery := coordinator.step_new( + name: 'error_recovery' + description: 'Recover from error' + f: fn (mut s flows.Step) ! { + println(' ✓ Error Step: Executing recovery...') + s.context['recovered'] = 'true' + } + )! + + // Main step that fails + mut main_step := coordinator.step_new( + name: 'failing_step' + description: 'This step will fail' + f: fn (mut s flows.Step) ! { + println(' ✗ Main Step: Intentionally failing...') + return error('Simulated error for testing') + } + )! + + main_step.error_step_add(error_recovery) + + // Run and expect error + coordinator.run() or { + println(' ✓ Error caught as expected: ${err.msg()}') + } + + // Verify error state in Redis + error_state := coordinator.get_step_state('failing_step')! + assert error_state['status'] == 'error', 'Expected step to be in error state' + + recovery_state := coordinator.get_step_state('error_recovery')! + assert recovery_state['status'] == 'success', 'Expected error step to execute' + + println(' ✓ Test 2 PASSED: Error handling works correctly') + coordinator.clear_redis()! +} + +fn test_multiple_next_steps() ! { + mut redis := redisclient.core_get()! + redis.flushdb()! + + mut coordinator := flows.new( + name: 'test_parallel_steps', + redis: redis, + ai: none + )! + + // Parent step + mut parent := coordinator.step_new( + name: 'parent_step' + description: 'Parent step with multiple children' + f: fn (mut s flows.Step) ! { + println(' ✓ Parent Step: Executing...') + } + )! + + // Child steps + mut child1 := coordinator.step_new( + name: 'child_step_1' + description: 'First child' + f: fn (mut s flows.Step) ! { + println(' ✓ Child Step 1: Executing...') + } + )! + + mut child2 := coordinator.step_new( + name: 'child_step_2' + description: 'Second child' + f: fn (mut s flows.Step) ! { + println(' ✓ Child Step 2: Executing...') + } + )! + + mut child3 := coordinator.step_new( + name: 'child_step_3' + description: 'Third child' + f: fn (mut s flows.Step) ! { + println(' ✓ Child Step 3: Executing...') + } + )! + + // Add multiple next steps + parent.next_step_add(child1) + parent.next_step_add(child2) + parent.next_step_add(child3) + + coordinator.run()! + + // Verify all steps executed + all_states := coordinator.get_all_steps_state()! + assert all_states.len >= 4, 'Expected 4 steps to execute' + + println(' ✓ Test 3 PASSED: Multiple next steps executed sequentially') + coordinator.clear_redis()! +} + +fn test_redis_state() ! { + mut redis := redisclient.core_get()! + redis.flushdb()! + + mut coordinator := flows.new( + name: 'test_redis_state', + redis: redis, + ai: none + )! + + mut step1 := coordinator.step_new( + name: 'redis_test_step' + description: 'Test Redis state storage' + f: fn (mut s flows.Step) ! { + println(' ✓ Executing step with context...') + s.context['user'] = 'test_user' + s.context['action'] = 'test_action' + } + )! + + coordinator.run()! + + // Retrieve state from Redis + step_state := coordinator.get_step_state('redis_test_step')! + + println(' Step state in Redis:') + for key, value in step_state { + println(' ${key}: ${value}') + } + + // Verify fields + assert step_state['name'] == 'redis_test_step', 'Step name mismatch' + assert step_state['status'] == 'success', 'Step status should be success' + assert step_state['description'] == 'Test Redis state storage', 'Description mismatch' + + // Verify JSON is stored + if json_data := step_state['json'] { + println(' ✓ JSON data stored in Redis: ${json_data[0..50]}...') + } + + // Verify log count + logs_count := step_state['logs_count'] or { '0' } + println(' ✓ Logs count: ${logs_count}') + + println(' ✓ Test 4 PASSED: Redis state correctly stored and retrieved') + coordinator.clear_redis()! +} + +fn test_complex_flow() ! { + mut redis := redisclient.core_get()! + redis.flushdb()! + + mut coordinator := flows.new( + name: 'test_complex_flow', + redis: redis, + ai: none + )! + + // Step 1: Validate + mut validate := coordinator.step_new( + name: 'validate_input' + description: 'Validate input parameters' + f: fn (mut s flows.Step) ! { + println(' ✓ Validating input...') + s.context['validated'] = 'true' + } + )! + + // Step 2: Transform (next step after validate) + mut transform := coordinator.step_new( + name: 'transform_data' + description: 'Transform input data' + f: fn (mut s flows.Step) ! { + println(' ✓ Transforming data...') + s.context['transformed'] = 'true' + } + )! + + // Step 3a: Save to DB (next step after transform) + mut save_db := coordinator.step_new( + name: 'save_to_database' + description: 'Save data to database' + f: fn (mut s flows.Step) ! { + println(' ✓ Saving to database...') + s.context['saved'] = 'true' + } + )! + + // Step 3b: Send notification (next step after transform) + mut notify := coordinator.step_new( + name: 'send_notification' + description: 'Send notification' + f: fn (mut s flows.Step) ! { + println(' ✓ Sending notification...') + s.context['notified'] = 'true' + } + )! + + // Step 4: Cleanup (final step) + mut cleanup := coordinator.step_new( + name: 'cleanup' + description: 'Cleanup resources' + f: fn (mut s flows.Step) ! { + println(' ✓ Cleaning up...') + s.context['cleaned'] = 'true' + } + )! + + // Build the flow chain + validate.next_step_add(transform) + transform.next_step_add(save_db) + transform.next_step_add(notify) + save_db.next_step_add(cleanup) + notify.next_step_add(cleanup) + + coordinator.run()! + + // Verify all steps executed + all_states := coordinator.get_all_steps_state()! + println(' Total steps executed: ${all_states.len}') + + for state in all_states { + name := state['name'] or { 'unknown' } + status := state['status'] or { 'unknown' } + duration := state['duration'] or { '0' } + println(' - ${name}: ${status} (${duration}ms)') + } + + assert all_states.len >= 5, 'Expected at least 5 steps' + + println(' ✓ Test 5 PASSED: Complex flow executed successfully') + coordinator.clear_redis()! +} \ No newline at end of file diff --git a/lib/core/code/example.v b/lib/core/code/example.v deleted file mode 100644 index c9d5828d..00000000 --- a/lib/core/code/example.v +++ /dev/null @@ -1,3 +0,0 @@ -module code - -pub type Value = string diff --git a/lib/core/code/improvements.md b/lib/core/code/improvements.md deleted file mode 100644 index 7fb31e66..00000000 --- a/lib/core/code/improvements.md +++ /dev/null @@ -1,247 +0,0 @@ -# Code Review and Improvement Plan for HeroLib Code Module - -## Overview - -The HeroLib `code` module provides utilities for parsing and generating V language code. It's designed to be a lightweight alternative to `v.ast` for code analysis and generation across multiple languages. While the module has good foundational structure, there are several areas that need improvement. - -## Issues Identified - -### 1. Incomplete TypeScript Generation Support - -- The `typescript()` method exists in some models but lacks comprehensive implementation -- Missing TypeScript generation for complex types (arrays, maps, results) -- No TypeScript interface generation for structs - -### 2. Template System Issues - -- Some templates are empty (e.g., `templates/function/method.py`, `templates/comment/comment.py`) -- Template usage is inconsistent across the codebase -- No clear separation between V and other language templates - -### 3. Missing Parser Documentation Examples - -- README.md mentions codeparser but doesn't show how to use the parser from this module -- No clear examples of parsing V files or modules - -### 4. Incomplete Type Handling - -- The `parse_type` function doesn't handle all V language types comprehensively -- Missing support for function types, sum types, and complex generics -- No handling of optional types (`?Type`) - -### 5. Code Structure and Consistency - -- Some functions lack proper error handling -- Inconsistent naming conventions in test files -- Missing documentation for several key functions - -## Improvement Plan - -### 1. Complete TypeScript Generation Implementation - -**What needs to be done:** - -- Implement comprehensive TypeScript generation in `model_types.v` -- Add TypeScript generation for all type variants -- Create proper TypeScript interface generation in `model_struct.v` - -**Specific fixes:** - -```v -// In model_types.v, improve the typescript() method: -pub fn (t Type) typescript() string { - return match t { - Map { 'Record' } - Array { '${t.typ.typescript()}[]' } - Object { t.name } - Result { 'Promise<${t.typ.typescript()}>' } // Better representation for async operations - Boolean { 'boolean' } - Integer { 'number' } - Alias { t.name } - String { 'string' } - Function { '(...args: any[]) => any' } // More appropriate for function types - Void { 'void' } - } -} - -// In model_struct.v, improve the typescript() method: -pub fn (s Struct) typescript() string { - name := texttools.pascal_case(s.name) - fields := s.fields.map(it.typescript()).join('\n ') - return 'export interface ${name} {\n ${fields}\n}' -} -``` - -### 2. Fix Template System - -**What needs to be done:** - -- Remove empty Python template files -- Ensure all templates are properly implemented -- Add template support for other languages - -**Specific fixes:** - -- Delete `templates/function/method.py` and `templates/comment/comment.py` if they're not needed -- Add proper TypeScript templates for struct and interface generation -- Create consistent template naming conventions - -### 3. Improve Parser Documentation - -**What needs to be done:** - -- Add clear examples in README.md showing how to use the parser -- Document the parsing functions with practical examples - -**Specific fixes:** -Add to README.md: - -```markdown -## Parsing V Code - -The code module provides utilities to parse V code into structured models: - -```v -import incubaid.herolib.core.code - -// Parse a V file -content := os.read_file('example.v') or { panic(err) } -vfile := code.parse_vfile(content) or { panic(err) } - -// Access parsed information -println('Module: ${vfile.mod}') -println('Number of functions: ${vfile.functions().len}') -println('Number of structs: ${vfile.structs().len}') - -// Parse individual components -function := code.parse_function(fn_code_string) or { panic(err) } -struct_ := code.parse_struct(struct_code_string) or { panic(err) } -``` - -### 4. Complete Type Handling - -**What needs to be done:** - -- Extend `parse_type` to handle more complex V types -- Add support for optional types (`?Type`) -- Improve generic type parsing - -**Specific fixes:** - -```v -// In model_types.v, enhance parse_type function: -pub fn parse_type(type_str string) Type { - mut type_str_trimmed := type_str.trim_space() - - // Handle optional types - if type_str_trimmed.starts_with('?') { - return Optional{parse_type(type_str_trimmed.all_after('?'))} - } - - // Handle function types - if type_str_trimmed.starts_with('fn ') { - // Parse function signature - return Function{} - } - - // Handle sum types - if type_str_trimmed.contains('|') { - types := type_str_trimmed.split('|').map(parse_type(it.trim_space())) - return Sum{types} - } - - // Existing parsing logic... -} -``` - -### 5. Code Structure Improvements - -**What needs to be done:** - -- Add proper error handling to all parsing functions -- Standardize naming conventions -- Improve documentation consistency - -**Specific fixes:** - -- Add error checking in `parse_function`, `parse_struct`, and other parsing functions -- Ensure all public functions have clear documentation comments -- Standardize test function names - -## Module Generation to Other Languages - -### Current Implementation - -The current code shows basic TypeScript generation support, but it's incomplete. The generation should: - -1. **Support multiple languages**: The code structure allows for multi-language generation, but only TypeScript has partial implementation -2. **Use templates consistently**: All language generation should use the template system -3. **Separate language-specific code**: Each language should have its own generation module - -### What Needs to Move to Other Modules - -**TypeScript Generation Module:** - -- Move all TypeScript-specific generation code to a new `typescript` module -- Create TypeScript templates for structs, interfaces, and functions -- Add proper TypeScript formatting support - -**Example Structure:** - -``` -lib/core/code/ -├── model_types.v # Core type models (language agnostic) -├── model_struct.v # Core struct/function models (language agnostic) -└── typescript/ # TypeScript-specific generation - ├── generator.v # TypeScript generation logic - └── templates/ # TypeScript templates -``` - -### Parser Usage Examples (to add to README.md) - -```v -// Parse a V file into a structured representation -content := os.read_file('mymodule/example.v') or { panic(err) } -vfile := code.parse_vfile(content)! - -// Extract all functions -functions := vfile.functions() -println('Found ${functions.len} functions') - -// Extract all structs -structs := vfile.structs() -for s in structs { - println('Struct: ${s.name}') - for field in s.fields { - println(' Field: ${field.name} (${field.typ.symbol()})') - } -} - -// Find a specific function -if greet_fn := vfile.get_function('greet') { - println('Found function: ${greet_fn.name}') - println('Parameters: ${greet_fn.params.map(it.name)}') - println('Returns: ${greet_fn.result.typ.symbol()}') -} - -// Parse a function from string -fn_code := ' -pub fn add(a int, b int) int { - return a + b -} -' -function := code.parse_function(fn_code)! -println('Parsed function: ${function.name}') -``` - -## Summary of Required Actions - -1. **Implement complete TypeScript generation** across all model types -2. **Remove empty template files** and organize templates properly -3. **Enhance type parsing** to handle optional types, function types, and sum types -4. **Add comprehensive parser documentation** with practical examples to README.md -5. **Create language-specific generation modules** to separate concerns -6. **Improve error handling** in all parsing functions -7. **Standardize documentation and naming** conventions across the module - -These improvements will make the code module more robust, easier to use, and better prepared for multi-language code generation. diff --git a/lib/core/code/model_example.v b/lib/core/code/model_example.v index 987e7874..d69ee5d0 100644 --- a/lib/core/code/model_example.v +++ b/lib/core/code/model_example.v @@ -6,4 +6,4 @@ pub struct Example { result Value } -// pub type Value = string +pub type Value = string diff --git a/lib/core/code/templates/comment/comment.py b/lib/core/code/templates/comment/comment.py deleted file mode 100644 index e69de29b..00000000 diff --git a/lib/core/flows/coordinator.v b/lib/core/flows/coordinator.v index 248e5b13..9deb6bfa 100644 --- a/lib/core/flows/coordinator.v +++ b/lib/core/flows/coordinator.v @@ -19,17 +19,30 @@ pub mut: current_step string // links to steps dict steps map[string]&Step logger logger.Logger - ai aiclient.AIClient + ai ?aiclient.AIClient redis ?&redisclient.Redis } -pub fn new() !Coordinator { +@[params] +pub struct CoordinatorArgs { +pub mut: + name string @[required] + redis ?&redisclient.Redis + ai ?aiclient.AIClient = none +} + +pub fn new(args CoordinatorArgs) !Coordinator { + ai := args.ai + return Coordinator{ + name: args.name logger: logger.new(path: '/tmp/flowlogger')! - ai: aiclient.new()! + ai: ai + redis: args.redis } } + @[params] pub struct StepNewArgs { pub mut: @@ -37,8 +50,8 @@ pub mut: description string f fn (mut s Step) ! @[required] context map[string]string - error_steps []Step - next_steps []Step + error_steps []string + next_steps []string error string params paramsparser.Params } diff --git a/lib/core/flows/run.v b/lib/core/flows/run.v index 2303e265..b2f286df 100644 --- a/lib/core/flows/run.v +++ b/lib/core/flows/run.v @@ -9,7 +9,7 @@ pub fn (mut c Coordinator) run() ! { } // Run a single step, including error and next steps -pub fn (mut c Coordinator) run_step(mut step Step) ! { +pub fn (mut c Coordinator) run_step(mut step &Step) ! { // Initialize step step.status = .running step.started_at = ostime.now().unix_milli() @@ -17,8 +17,8 @@ pub fn (mut c Coordinator) run_step(mut step Step) ! { // Log step start step.log( - level: .info - message: 'Step "${step.name}" started' + logtype: .stdout + log: 'Step "${step.name}" started' )! // Execute main step function @@ -30,13 +30,16 @@ pub fn (mut c Coordinator) run_step(mut step Step) ! { step.store_redis()! step.log( - level: .error - message: 'Step "${step.name}" failed: ${err.msg()}' + logtype: .error + log: 'Step "${step.name}" failed: ${err.msg()}' )! // Run error steps if any if step.error_steps.len > 0 { - for mut error_step in step.error_steps { + for error_step_name in step.error_steps { + mut error_step := c.steps[error_step_name] or { + return error('Error step "${error_step_name}" not found in coordinator "${c.name}"') + } c.run_step(mut error_step)! } } @@ -50,13 +53,16 @@ pub fn (mut c Coordinator) run_step(mut step Step) ! { step.store_redis()! step.log( - level: .info - message: 'Step "${step.name}" completed successfully' + logtype: .stdout + log: 'Step "${step.name}" completed successfully' )! // Run next steps if any if step.next_steps.len > 0 { - for mut next_step in step.next_steps { + for next_step_name in step.next_steps { + mut next_step := c.steps[next_step_name] or { + return error('Next step "${next_step_name}" not found in coordinator "${c.name}"') + } c.run_step(mut next_step)! } } @@ -64,7 +70,7 @@ pub fn (mut c Coordinator) run_step(mut step Step) ! { // Get step state from redis pub fn (c Coordinator) get_step_state(step_name string) !map[string]string { - if redis := c.redis { + if mut redis := c.redis { return redis.hgetall('flow:${c.name}:${step_name}')! } return error('Redis not configured') @@ -73,7 +79,7 @@ pub fn (c Coordinator) get_step_state(step_name string) !map[string]string { // Get all steps state from redis (for UI dashboard) pub fn (c Coordinator) get_all_steps_state() ![]map[string]string { mut states := []map[string]string{} - if redis := c.redis { + if mut redis := c.redis { pattern := 'flow:${c.name}:*' keys := redis.keys(pattern)! for key in keys { @@ -85,7 +91,7 @@ pub fn (c Coordinator) get_all_steps_state() ![]map[string]string { } pub fn (c Coordinator) clear_redis() ! { - if redis := c.redis { + if mut redis := c.redis { pattern := 'flow:${c.name}:*' keys := redis.keys(pattern)! for key in keys { diff --git a/lib/core/flows/step.v b/lib/core/flows/step.v index 68ddac93..42f0c8e4 100644 --- a/lib/core/flows/step.v +++ b/lib/core/flows/step.v @@ -2,6 +2,8 @@ module flows import incubaid.herolib.data.paramsparser import incubaid.herolib.core.logger +import time as ostime +import json pub enum StepStatus { pending @@ -21,23 +23,71 @@ pub mut: description string main_step fn (mut s Step) ! @[required] context map[string]string - error_steps []Step - next_steps []Step + error_steps []string + next_steps []string error string logs []logger.LogItem params paramsparser.Params coordinator &Coordinator } -pub fn (mut s Step) error_step_add(s2 Step) { - s.error_steps << s2 +pub fn (mut s Step) error_step_add(s2 &Step) { + s.error_steps << s2.name } -pub fn (mut s Step) next_step_add(s2 Step) { - s.next_steps << s2 +pub fn (mut s Step) next_step_add(s2 &Step) { + s.next_steps << s2.name } pub fn (mut s Step) log(l logger.LogItemArgs) ! { mut l2 := s.coordinator.logger.log(l)! s.logs << l2 } + + +pub fn (mut s Step) store_redis() ! { + if mut redis := s.coordinator.redis { + key := 'flow:${s.coordinator.name}:${s.name}' + + redis.hset(key, 'name', s.name)! + redis.hset(key, 'description', s.description)! + redis.hset(key, 'status', s.status.str())! + redis.hset(key, 'error', s.error_msg)! + redis.hset(key, 'logs_count', s.logs.len.str())! + redis.hset(key, 'started_at', s.started_at.str())! + redis.hset(key, 'finished_at', s.finished_at.str())! + redis.hset(key, 'json', s.to_json()!)! + + // Set expiration to 24 hours + redis.expire(key, 86400)! + } +} + + +@[json: id] +pub struct StepJSON { +pub: + name string + description string + status string + error string + logs_count int + started_at i64 + finished_at i64 + duration i64 // milliseconds +} + +pub fn (s Step) to_json() !string { + duration := s.finished_at - s.started_at + step_json := StepJSON{ + name: s.name + description: s.description + status: s.status.str() + error: s.error_msg + logs_count: s.logs.len + started_at: s.started_at + finished_at: s.finished_at + duration: duration + } + return json.encode(step_json) +}