rails-openapi-gen 0.0.2 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +40 -0
  3. data/CLAUDE.md +17 -5
  4. data/README.md +25 -0
  5. data/lib/rails-openapi-gen/ast_nodes/array_node.rb +101 -0
  6. data/lib/rails-openapi-gen/ast_nodes/base_node.rb +139 -0
  7. data/lib/rails-openapi-gen/ast_nodes/comment_data.rb +180 -0
  8. data/lib/rails-openapi-gen/ast_nodes/node_factory.rb +206 -0
  9. data/lib/rails-openapi-gen/ast_nodes/object_node.rb +129 -0
  10. data/lib/rails-openapi-gen/ast_nodes/partial_node.rb +111 -0
  11. data/lib/rails-openapi-gen/ast_nodes/property_node.rb +74 -0
  12. data/lib/rails-openapi-gen/ast_nodes.rb +129 -0
  13. data/lib/rails-openapi-gen/configuration.rb +154 -22
  14. data/lib/rails-openapi-gen/debug_helpers.rb +185 -0
  15. data/lib/rails-openapi-gen/engine.rb +1 -1
  16. data/lib/rails-openapi-gen/generators/yaml_generator.rb +242 -27
  17. data/lib/rails-openapi-gen/generators.rb +5 -0
  18. data/lib/rails-openapi-gen/importer.rb +164 -145
  19. data/lib/rails-openapi-gen/parsers/comment_parser.rb +1 -1
  20. data/lib/rails-openapi-gen/parsers/comment_parsers/attribute_parser.rb +7 -7
  21. data/lib/rails-openapi-gen/parsers/comment_parsers/base_attribute_parser.rb +5 -9
  22. data/lib/rails-openapi-gen/parsers/comment_parsers/body_parser.rb +6 -6
  23. data/lib/rails-openapi-gen/parsers/comment_parsers/conditional_parser.rb +1 -1
  24. data/lib/rails-openapi-gen/parsers/comment_parsers/operation_parser.rb +5 -5
  25. data/lib/rails-openapi-gen/parsers/comment_parsers/param_parser.rb +6 -6
  26. data/lib/rails-openapi-gen/parsers/comment_parsers/query_parser.rb +6 -6
  27. data/lib/rails-openapi-gen/parsers/controller_parser.rb +64 -20
  28. data/lib/rails-openapi-gen/parsers/jbuilder/ast_parser.rb +914 -0
  29. data/lib/rails-openapi-gen/parsers/jbuilder/call_detectors/array_call_detector.rb +103 -0
  30. data/lib/rails-openapi-gen/parsers/jbuilder/call_detectors/base_detector.rb +107 -0
  31. data/lib/rails-openapi-gen/parsers/jbuilder/call_detectors/cache_call_detector.rb +112 -0
  32. data/lib/rails-openapi-gen/parsers/jbuilder/call_detectors/json_call_detector.rb +91 -0
  33. data/lib/rails-openapi-gen/parsers/jbuilder/call_detectors/key_format_detector.rb +27 -0
  34. data/lib/rails-openapi-gen/parsers/jbuilder/call_detectors/null_handling_detector.rb +27 -0
  35. data/lib/rails-openapi-gen/parsers/jbuilder/call_detectors/object_manipulation_detector.rb +27 -0
  36. data/lib/rails-openapi-gen/parsers/jbuilder/call_detectors/partial_call_detector.rb +125 -0
  37. data/lib/rails-openapi-gen/parsers/jbuilder/call_detectors.rb +95 -0
  38. data/lib/rails-openapi-gen/parsers/jbuilder/jbuilder_parser.rb +39 -0
  39. data/lib/rails-openapi-gen/parsers/jbuilder/operation_comment_parser.rb +26 -0
  40. data/lib/rails-openapi-gen/parsers/jbuilder/processors/array_processor.rb +266 -0
  41. data/lib/rails-openapi-gen/parsers/jbuilder/processors/base_processor.rb +235 -0
  42. data/lib/rails-openapi-gen/parsers/jbuilder/processors/composite_processor.rb +97 -0
  43. data/lib/rails-openapi-gen/parsers/jbuilder/processors/object_processor.rb +176 -0
  44. data/lib/rails-openapi-gen/parsers/jbuilder/processors/partial_processor.rb +69 -0
  45. data/lib/rails-openapi-gen/parsers/jbuilder/processors/property_processor.rb +68 -0
  46. data/lib/rails-openapi-gen/parsers/jbuilder/processors.rb +10 -0
  47. data/lib/rails-openapi-gen/parsers/jbuilder/property_comment_parser.rb +26 -0
  48. data/lib/rails-openapi-gen/parsers/jbuilder.rb +10 -0
  49. data/lib/rails-openapi-gen/parsers/routes_parser.rb +83 -9
  50. data/lib/rails-openapi-gen/parsers/template_processors/jbuilder_template_processor.rb +125 -131
  51. data/lib/rails-openapi-gen/parsers/template_processors/response_template_processor.rb +8 -12
  52. data/lib/rails-openapi-gen/parsers/template_processors.rb +6 -0
  53. data/lib/rails-openapi-gen/parsers.rb +9 -0
  54. data/lib/rails-openapi-gen/processors/ast_to_schema_processor.rb +226 -0
  55. data/lib/rails-openapi-gen/processors/base_processor.rb +124 -0
  56. data/lib/rails-openapi-gen/processors/component_schema_processor.rb +35 -0
  57. data/lib/rails-openapi-gen/processors/openapi_schema_processor.rb +218 -0
  58. data/lib/rails-openapi-gen/processors.rb +7 -0
  59. data/lib/rails-openapi-gen/railtie.rb +1 -1
  60. data/lib/rails-openapi-gen/tasks/openapi.rake +4 -4
  61. data/lib/rails-openapi-gen/version.rb +1 -1
  62. data/lib/rails-openapi-gen.rb +169 -196
  63. data/lib/tasks/openapi_import.rake +35 -36
  64. data/rails-openapi-gen.gemspec +6 -5
  65. metadata +62 -23
  66. data/lib/rails-openapi-gen/parsers/jbuilder_parser.rb +0 -529
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 862b4bda485ef4f20970eb00a11b6d9c7c52215b9013a4d5596fc2101914d48b
4
- data.tar.gz: 9f34b642c0ce919ac0384e6ffdc9c0cfa6073f4ab33c827f599db0bcdbed597b
3
+ metadata.gz: 0d59044c5ff98fab8b4ca1e9de7dd53e71eea91ffa2d5a8d37f0acb9f335f059
4
+ data.tar.gz: cc88029ac40811d0181ca65892ea8c09dd43134dd08a2d668bcd0285e2003f3d
5
5
  SHA512:
6
- metadata.gz: 1b1a0e43790fd0094a4831b0abb69c3646f849bcebc378329b0d156f248f9a393f1066f5d7ebb2a39b77247d666ab07df82e892537fe707a23072176e6b913f0
7
- data.tar.gz: e9510642fbb5a8a37da2864778ea926bd1c0317b33700aa9299ce9ebfbe495a6443e8a61e0e265277a21c8efdc61669fd64f4a2231d8234739844c0b249c6022
6
+ metadata.gz: 2578b9fd575a89a3432ea01f64e86e8f4e6922343f606c8f246a092f5a7a6dfe8da98b47717159238982f0140377ebdfdf0dc90bac879ba2b14727782e5ca297
7
+ data.tar.gz: 0cc4b985a493cf04edb7d1cb689e24d244ac33572d457f7b0c8fea49d8562a2c778ee13c5972fe14ddecd09ba026ee943edcc2c5924c6585717ce4952b69eff7
data/CHANGELOG.md ADDED
@@ -0,0 +1,40 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ ## [0.0.3] - 2025-01-13
6
+
7
+ ### Added
8
+ - Ruby 3.1 compatibility with intelligent fallback parsing system
9
+ - Robust error handling for Parser gem version mismatches
10
+ - Support for root array structures in Jbuilder templates
11
+ - Component reference system for partials
12
+ - Enhanced debug output with RAILS_OPENAPI_DEBUG environment variable
13
+
14
+ ### Fixed
15
+ - Parser gem compatibility issues between Ruby 3.1.3 and 3.1.7
16
+ - RuboCop compliance with all Lint rules enabled
17
+ - Test failures related to boolean symbols and unused variables
18
+ - Proper handling of nested partials and array structures
19
+ - Component naming conflicts in test environments
20
+
21
+ ### Changed
22
+ - Parser gem dependency updated to ~> 3.1.0 for better compatibility
23
+ - Improved AST parsing with multiple fallback strategies
24
+ - Enhanced error messages and debugging capabilities
25
+ - Removed project-specific naming patterns from tests
26
+
27
+ ## [0.0.2] - Previous version
28
+
29
+ ### Initial features
30
+ - Basic OpenAPI generation from Rails routes
31
+ - Jbuilder template parsing with @openapi comments
32
+ - Controller action detection
33
+ - YAML output generation
34
+
35
+ ## [0.0.1] - Initial release
36
+
37
+ ### Features
38
+ - Initial implementation of rails-openapi-gen
39
+ - Basic route parsing
40
+ - Simple Jbuilder template support
data/CLAUDE.md CHANGED
@@ -6,12 +6,18 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
6
6
 
7
7
  Rails OpenAPI Gen is a Ruby gem that automatically generates OpenAPI 3.0 specifications from Rails applications. It uses AST parsing to analyze routes, controllers, and Jbuilder templates, relying on `# @openapi` comments as the source of truth for type information.
8
8
 
9
+ **Requirements:**
10
+ - Ruby 3.0 or higher
11
+ - Rails 6.0 or higher
12
+ - Parser gem 3.0+ for AST processing
13
+
9
14
  ## Development Commands
10
15
 
11
16
  **Testing:**
12
17
  ```bash
13
- rspec # Run all tests
14
- rspec spec/specific_spec.rb # Run specific test file
18
+ bundle exec rspec # Run all tests (required for parser gem dependencies)
19
+ bundle exec rspec spec/specific_spec.rb # Run specific test file
20
+ rspec # Alternative (may have parser gem loading issues)
15
21
  ```
16
22
 
17
23
  **Linting:**
@@ -29,7 +35,7 @@ bin/rails openapi:check # Check for missing comments and uncommitted changes
29
35
 
30
36
  **Gem Development:**
31
37
  ```bash
32
- bundle install # Install dependencies
38
+ bundle install # Install dependencies (requires Ruby 3.0+)
33
39
  rake -T # List available rake tasks
34
40
  ```
35
41
 
@@ -66,19 +72,25 @@ rake -T # List available rake tasks
66
72
  The gem uses `# @openapi` comments in Jbuilder templates:
67
73
 
68
74
  ```ruby
69
- # @openapi id:integer required:true description:"User ID"
75
+ # @openapi id:integer description:"User ID"
70
76
  json.id @user.id
71
77
 
72
78
  # @openapi status:string enum:[active,inactive] description:"User status"
73
79
  json.status @user.status
80
+
81
+ # @openapi email:string required:false description:"Optional email"
82
+ json.email @user.email
74
83
  ```
75
84
 
76
85
  **Supported attributes:**
77
86
  - `type`: Data type (string, integer, boolean, number, array, object)
78
- - `required`: Whether field is required (true/false)
87
+ - `required`: Whether field is required (false to make optional; defaults to required)
79
88
  - `enum`: Allowed values for the field
80
89
  - `description`: Human-readable description
81
90
 
91
+ **Required Field Behavior:**
92
+ Properties are **required by default** unless explicitly marked with `required:false`. This ensures comprehensive API documentation where all fields are documented as required unless specifically marked as optional.
93
+
82
94
  ### Conditional Rendering Support
83
95
 
84
96
  For properties that are conditionally rendered (e.g., inside `if` statements), use the `# @openapi conditional:true` comment:
data/README.md CHANGED
@@ -2,6 +2,31 @@
2
2
 
3
3
  Rails comment-driven OpenAPI specification generator.
4
4
 
5
+ ## Requirements
6
+
7
+ - **Ruby 3.0 or higher**
8
+ - **Rails 6.0 or higher**
9
+
10
+ ## Installation
11
+
12
+ Add this line to your application's Gemfile:
13
+
14
+ ```ruby
15
+ gem 'rails-openapi-gen'
16
+ ```
17
+
18
+ And then execute:
19
+
20
+ ```bash
21
+ $ bundle install
22
+ ```
23
+
24
+ Or install it yourself as:
25
+
26
+ ```bash
27
+ $ gem install rails-openapi-gen
28
+ ```
29
+
5
30
  ## Overview
6
31
 
7
32
  rails-openapi-gen analyzes your Rails application's routes.rb, controllers, and jbuilder templates to automatically generate OpenAPI documentation. It uses AST parsing to extract JSON structure and relies on `# @openapi` comments for accurate type information.
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsOpenapiGen::AstNodes
4
+ # Represents an array node in Jbuilder template (json.array! or json.property @collection)
5
+ class ArrayNode < BaseNode
6
+ attr_reader :property_name, :comment_data, :is_conditional, :is_root_array
7
+
8
+ def initialize(property_name: nil, comment_data: nil, is_conditional: false, is_root_array: false, parent: nil, metadata: {})
9
+ super(parent: parent, metadata: metadata)
10
+ @property_name = property_name || (is_root_array ? 'items' : nil)
11
+ @comment_data = comment_data || CommentData.new(type: 'array')
12
+ @is_conditional = is_conditional
13
+ @is_root_array = is_root_array
14
+ end
15
+
16
+ # Add an item property to this array
17
+ # @param item_node [BaseNode] Item node to add
18
+ # @return [BaseNode] The added item node
19
+ def add_item(item_node)
20
+ add_child(item_node)
21
+ end
22
+
23
+ # Get all item properties in this array
24
+ # @return [Array<BaseNode>] Array item nodes
25
+ def items
26
+ @children
27
+ end
28
+
29
+ # Check if this array is required in OpenAPI schema
30
+ # @return [Boolean] True if array is required
31
+ def required?
32
+ @comment_data.required? && !@is_conditional
33
+ end
34
+
35
+ # Check if this array is optional in OpenAPI schema
36
+ # @return [Boolean] True if array is optional
37
+ def optional?
38
+ !required?
39
+ end
40
+
41
+ # Get the OpenAPI type for array items
42
+ # @return [String] OpenAPI type for items
43
+ def item_type
44
+ return 'object' if items.any?
45
+
46
+ # Check comment data for item type specification
47
+ if @comment_data.items
48
+ case @comment_data.items
49
+ when Hash
50
+ @comment_data.items[:type] || 'object'
51
+ when String
52
+ @comment_data.items
53
+ else
54
+ 'object'
55
+ end
56
+ else
57
+ 'object'
58
+ end
59
+ end
60
+
61
+ # Get the description for this array
62
+ # @return [String, nil] Array description
63
+ def description
64
+ @comment_data.description
65
+ end
66
+
67
+ # Check if this is a root array (json.array! at template root)
68
+ # @return [Boolean] True if this is a root array
69
+ def root_array?
70
+ @is_root_array
71
+ end
72
+
73
+ # Convert to hash representation
74
+ # @return [Hash] Hash representation
75
+ def to_h
76
+ items_hash = items.map { |item| item.respond_to?(:to_h) ? item.to_h : item }
77
+ super.merge(
78
+ property_name: @property_name,
79
+ comment_data: @comment_data&.to_h,
80
+ is_conditional: @is_conditional,
81
+ is_root_array: @is_root_array,
82
+ required: required?,
83
+ openapi_type: 'array',
84
+ item_type: item_type,
85
+ description: description,
86
+ items: items_hash,
87
+ # Backward compatibility - also provide array_item_properties for Generator
88
+ array_item_properties: items_hash,
89
+ # Backward compatibility - also provide is_array_root for Generator
90
+ is_array_root: @is_root_array
91
+ ).compact
92
+ end
93
+
94
+ # Accept visitor for visitor pattern
95
+ # @param visitor [Object] Visitor object
96
+ # @return [Object] Result from visitor
97
+ def accept(visitor)
98
+ visitor.visit_array(self)
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,139 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsOpenapiGen
4
+ module AstNodes
5
+ # Base class for all AST nodes in the Jbuilder parser
6
+ # Provides common interface and basic functionality for tree structure
7
+ class BaseNode
8
+ attr_accessor :parent
9
+ attr_reader :children, :metadata
10
+
11
+ def initialize(parent: nil, metadata: {})
12
+ @parent = parent
13
+ @children = []
14
+ @metadata = metadata
15
+ end
16
+
17
+ # Add a child node to this node
18
+ # @param child [BaseNode] Child node to add
19
+ # @return [BaseNode] The added child node
20
+ def add_child(child)
21
+ child.parent = self if child.respond_to?(:parent=)
22
+ @children << child
23
+ child
24
+ end
25
+
26
+ # Remove a child node from this node
27
+ # @param child [BaseNode] Child node to remove
28
+ # @return [BaseNode, nil] The removed child node or nil
29
+ def remove_child(child)
30
+ @children.delete(child)
31
+ end
32
+
33
+ # Get all descendants (children and their children recursively)
34
+ # @return [Array<BaseNode>] All descendant nodes
35
+ def descendants
36
+ result = []
37
+ @children.each do |child|
38
+ result << child
39
+ result.concat(child.descendants) if child.respond_to?(:descendants)
40
+ end
41
+ result
42
+ end
43
+
44
+ # Check if this node is a leaf (has no children)
45
+ # @return [Boolean] True if this node has no children
46
+ def leaf?
47
+ @children.empty?
48
+ end
49
+
50
+ # Check if this node is the root (has no parent)
51
+ # @return [Boolean] True if this node has no parent
52
+ def root?
53
+ @parent.nil?
54
+ end
55
+
56
+ # Get the root node of the tree
57
+ # @return [BaseNode] The root node
58
+ def root
59
+ return self if root?
60
+
61
+ @parent.root
62
+ end
63
+
64
+ # Convert node to hash representation
65
+ # @return [Hash] Hash representation of the node
66
+ def to_h
67
+ {
68
+ node_type: self.class.name.split('::').last.downcase.gsub('node', ''),
69
+ metadata: @metadata,
70
+ children: @children.map { |child| child.respond_to?(:to_h) ? child.to_h : child }
71
+ }
72
+ end
73
+
74
+ # Accept a visitor for visitor pattern implementation
75
+ # @param visitor [Object] Visitor object
76
+ # @return [Object] Result from visitor
77
+ def accept(visitor)
78
+ visitor.visit(self)
79
+ end
80
+
81
+ # Pretty print the AST tree structure for debugging
82
+ # @param indent [Integer] Current indentation level
83
+ # @return [void]
84
+ def pretty_print(indent = 0)
85
+ pad = ' ' * indent
86
+ node_name = self.class.name.split('::').last
87
+ summary = summary_attributes
88
+
89
+ puts "#{pad}#{tree_symbol(indent)} #{node_name}#{summary.empty? ? '' : " (#{summary})"}"
90
+
91
+ @children.each_with_index do |child, _index|
92
+ child.pretty_print(indent + 1) if child.respond_to?(:pretty_print)
93
+ end
94
+ end
95
+
96
+ # Print just this node without children
97
+ # @return [String] Single line representation
98
+ def debug_line
99
+ node_name = self.class.name.split('::').last
100
+ summary = summary_attributes
101
+ "#{node_name}#{summary.empty? ? '' : " (#{summary})"}"
102
+ end
103
+
104
+ # Generate summary attributes for debugging display
105
+ # @return [String] Summary of key attributes
106
+ def summary_attributes
107
+ attrs = []
108
+
109
+ # Common attributes that most nodes might have
110
+ attrs << "name=#{property_name}" if respond_to?(:property_name) && property_name
111
+
112
+ if respond_to?(:comment_data) && comment_data
113
+ attrs << "type=#{comment_data.type}" if comment_data.type
114
+ attrs << "required=#{comment_data.required?}" if comment_data.respond_to?(:required?)
115
+ attrs << "desc=#{comment_data.description[0..30]}..." if comment_data.description && comment_data.description.length > 30
116
+ attrs << "desc=#{comment_data.description}" if comment_data.description && comment_data.description.length <= 30
117
+ end
118
+
119
+ attrs << "children=#{@children.size}" if @children.size > 0
120
+ attrs << "conditional" if respond_to?(:is_conditional) && is_conditional
121
+
122
+ attrs.join(', ')
123
+ end
124
+
125
+ private
126
+
127
+ # Generate tree symbol for pretty printing
128
+ # @param indent [Integer] Current indentation level
129
+ # @return [String] Tree symbol (├── or └──)
130
+ def tree_symbol(indent)
131
+ return '└─' if indent == 0
132
+
133
+ '├─'
134
+ end
135
+
136
+ # No longer need protected attr_writer since we have attr_accessor above
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,180 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsOpenapiGen::AstNodes
4
+ # Represents comment data parsed from @openapi annotations
5
+ # Encapsulates all OpenAPI-related metadata extracted from comments
6
+ class CommentData
7
+ attr_reader :type, :description, :required, :enum, :field_name, :items, :conditional, :format, :example
8
+
9
+ def initialize(
10
+ type: nil,
11
+ description: nil,
12
+ required: true,
13
+ enum: nil,
14
+ field_name: nil,
15
+ items: nil,
16
+ conditional: false,
17
+ format: nil,
18
+ example: nil
19
+ )
20
+ @type = type
21
+ @description = description
22
+ @required = required
23
+ @enum = enum
24
+ @field_name = field_name
25
+ @items = items
26
+ @conditional = conditional
27
+ @format = format
28
+ @example = example
29
+ end
30
+
31
+ # Check if the property is required
32
+ # @return [Boolean] True if property is required
33
+ def required?
34
+ @required != false && @required != 'false'
35
+ end
36
+
37
+ # Check if the property is optional
38
+ # @return [Boolean] True if property is optional
39
+ def optional?
40
+ !required?
41
+ end
42
+
43
+ # Check if the property is conditional
44
+ # @return [Boolean] True if property is conditional
45
+ def conditional?
46
+ @conditional == true || @conditional == 'true'
47
+ end
48
+
49
+ # Check if the property has enum values
50
+ # @return [Boolean] True if property has enum values
51
+ def has_enum?
52
+ @enum && !@enum.empty?
53
+ end
54
+
55
+ # Check if the property has a specific format
56
+ # @return [Boolean] True if property has format specification
57
+ def has_format?
58
+ # Auto-detect format from invalid types that were converted
59
+ return true if auto_format_from_invalid_type
60
+
61
+ @format && !@format.empty?
62
+ end
63
+
64
+ # Check if the property has an example
65
+ # @return [Boolean] True if property has example
66
+ def has_example?
67
+ @example
68
+ end
69
+
70
+ # Get OpenAPI type, defaulting to string if not specified
71
+ # @return [String] OpenAPI type
72
+ def openapi_type
73
+ # Handle common invalid types and auto-correct them
74
+ case @type
75
+ when 'date-time', 'datetime', 'date', 'time'
76
+ # These are not valid OpenAPI types, should be string with format
77
+ 'string'
78
+ else
79
+ @type || 'string'
80
+ end
81
+ end
82
+
83
+ # Get format for the property, including auto-detected formats
84
+ # @return [String, nil] Format specification
85
+ def format_value
86
+ # Auto-detect format from invalid types that were converted
87
+ auto_format = auto_format_from_invalid_type
88
+ return auto_format if auto_format
89
+
90
+ @format
91
+ end
92
+
93
+ # Get items specification for arrays
94
+ # @return [Hash, nil] Items specification
95
+ def array_items
96
+ return nil unless @type == 'array'
97
+
98
+ @items || { 'type' => 'object' }
99
+ end
100
+
101
+ # Convert to hash representation suitable for OpenAPI schema
102
+ # @return [Hash] Hash representation
103
+ def to_openapi_schema
104
+ schema = { 'type' => openapi_type }
105
+ schema['description'] = @description if @description
106
+ schema['enum'] = @enum if has_enum?
107
+ schema['format'] = format_value if has_format?
108
+ schema['example'] = @example if has_example?
109
+ schema['items'] = array_items if @type == 'array' && array_items
110
+ schema
111
+ end
112
+
113
+ # Convert to hash representation for internal use
114
+ # @return [Hash] Hash representation
115
+ def to_h
116
+ {
117
+ type: @type,
118
+ description: @description,
119
+ required: @required,
120
+ enum: @enum,
121
+ field_name: @field_name,
122
+ items: @items,
123
+ conditional: @conditional,
124
+ format: @format,
125
+ example: @example
126
+ }.compact
127
+ end
128
+
129
+ # Merge with another CommentData, giving precedence to non-nil values
130
+ # @param other [CommentData] Other comment data to merge
131
+ # @return [CommentData] New merged comment data
132
+ def merge(other)
133
+ return self unless other.is_a?(CommentData)
134
+
135
+ CommentData.new(
136
+ type: other.type || @type,
137
+ description: other.description || @description,
138
+ required: other.required.nil? ? @required : other.required,
139
+ enum: other.enum || @enum,
140
+ field_name: other.field_name || @field_name,
141
+ items: other.items || @items,
142
+ conditional: other.conditional.nil? ? @conditional : other.conditional,
143
+ format: other.format || @format,
144
+ example: other.example || @example
145
+ )
146
+ end
147
+
148
+ # Create a copy with updated attributes
149
+ # @param attributes [Hash] Attributes to update
150
+ # @return [CommentData] New comment data with updated attributes
151
+ def with(**attributes)
152
+ CommentData.new(
153
+ type: attributes.fetch(:type, @type),
154
+ description: attributes.fetch(:description, @description),
155
+ required: attributes.fetch(:required, @required),
156
+ enum: attributes.fetch(:enum, @enum),
157
+ field_name: attributes.fetch(:field_name, @field_name),
158
+ items: attributes.fetch(:items, @items),
159
+ conditional: attributes.fetch(:conditional, @conditional),
160
+ format: attributes.fetch(:format, @format),
161
+ example: attributes.fetch(:example, @example)
162
+ )
163
+ end
164
+
165
+ private
166
+
167
+ # Auto-detect format from invalid types that were converted to string
168
+ # @return [String, nil] Format specification
169
+ def auto_format_from_invalid_type
170
+ case @type
171
+ when 'date-time', 'datetime'
172
+ 'date-time'
173
+ when 'date'
174
+ 'date'
175
+ when 'time'
176
+ 'time'
177
+ end
178
+ end
179
+ end
180
+ end