comicinfo 1.0.0

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.
@@ -0,0 +1,255 @@
1
+ # Development Guidelines
2
+
3
+ ## Project Overview
4
+ This Ruby gem provides an idiomatic interface for working with ComicInfo.xml files,
5
+ following the official ComicInfo schema specifications from the Anansi Project.
6
+
7
+ ## Development Philosophy
8
+ - **Test-Driven Development**: Write failing tests first, then implement functionality
9
+ - **Idiomatic Ruby**: Follow Ruby conventions and best practices
10
+ - **Schema Compliance**: Strict adherence to ComicInfo XSD schema definitions
11
+ - **Error Handling**: Graceful handling of malformed XML and invalid data
12
+
13
+ ## Code Standards
14
+
15
+ ### Ruby Version
16
+ - Minimum required: Ruby 3.4.6+
17
+ - Use modern Ruby features and syntax patterns
18
+
19
+ ### Dependencies
20
+ - **nokogiri** (>= 1.18.10): XML parsing and generation
21
+ - **rspec** (>= 3.13.1): Testing framework
22
+ - **rubocop** (>= 1.81.1): Code linting and formatting
23
+
24
+ ### Code Style
25
+ - Follow RuboCop rules (configured in `.rubocop.yml`)
26
+ - Use 2-space indentation
27
+
28
+ ### Testing Guidelines
29
+ - Use RSpec for all tests
30
+ - Create fixture files for test data (no inline XML strings)
31
+ - Test both happy path and error conditions
32
+ - Aim for >95% test coverage
33
+ - Group related tests in describe/context blocks
34
+ - Use descriptive test descriptions
35
+ - **Code Alignment**: Align similar parts of lines only within the same method or `it` block
36
+ - **Test Structure**: Align expect statements within individual test cases for readability
37
+
38
+ ### File Organization
39
+ ```
40
+ lib/
41
+ ├─ comicinfo.rb # Main module and autoloads
42
+ ├─ comicinfo/
43
+ │ ├─ version.rb # Gem version constant
44
+ │ ├─ issue.rb # Main ComicInfo::Issue class
45
+ │ ├─ page.rb # ComicInfo::Page class
46
+ │ ├─ enums.rb # Schema enum definitions
47
+ │ └─ errors.rb # Custom exception classes
48
+ spec/
49
+ ├─ spec_helper.rb # RSpec configuration with FixtureHelpers
50
+ ├─ comic_info_spec.rb # Main module specs
51
+ ├─ comic_info/
52
+ │ ├─ issue_spec.rb # ComicInfo::Issue class specs
53
+ │ └─ page_spec.rb # ComicInfo::Page class specs
54
+ └─ fixtures/ # XML test fixtures
55
+ │ ├─ valid_minimal.xml
56
+ │ ├─ valid_complete.xml
57
+ │ ├─ invalid_xml.xml
58
+ │ └─ edge_cases/
59
+ ```
60
+
61
+ ## API Design Principles
62
+
63
+ ### Class Interface
64
+ ```ruby
65
+ # Primary interface
66
+ ComicInfo.load file_path_or_xml_string #=> ComicInfo::Issue instance
67
+ ComicInfo::Issue.new xml_string #=> ComicInfo::Issue instance
68
+
69
+ # Instance methods match schema fields
70
+ comic.title #=> String
71
+ comic.series #=> String
72
+ comic.count #=> Integer
73
+ comic.pages #=> Array<ComicInfo::Page>
74
+ comic.black_and_white #=> String (enum value)
75
+
76
+ # Multi-value fields have both singular and plural methods
77
+ comic.character #=> String (comma-separated)
78
+ comic.characters #=> Array<String>
79
+ comic.genre #=> String (comma-separated)
80
+ comic.genres #=> Array<String>
81
+ comic.web #=> String (space-separated URLs)
82
+ comic.web_urls #=> Array<String>
83
+ ```
84
+
85
+ ### Naming Conventions
86
+ - Class names: `ComicInfo::Issue`, `ComicInfo::Page`
87
+ - Method names: snake_case following Ruby conventions
88
+ - Schema field mapping:
89
+ - XML `<Title>` → Ruby `#title`
90
+ - XML `<BlackAndWhite>` → Ruby `#black_and_white`
91
+ - XML `<CommunityRating>` → Ruby `#community_rating`
92
+ - Multi-value fields:
93
+ - Singular method returns original string from XML
94
+ - Plural method returns array of split values
95
+ - XML `<Characters>` → Ruby `#character` (string) & `#characters` (array)
96
+ - XML `<Teams>` → Ruby `#team` (string) & `#teams` (array)
97
+
98
+ ### Error Handling
99
+ - Define custom exception classes in `ComicInfo::Errors`
100
+ - Validate enum values against schema definitions
101
+ - Provide meaningful error messages for invalid data
102
+ - Handle XML parsing errors gracefully
103
+
104
+ ## Development Workflow
105
+
106
+ ### Development Process
107
+ 1. Write failing tests with fixture files
108
+ 2. Implement minimal code to make tests pass
109
+ 3. Refactor for clarity and performance
110
+ 4. Run RuboCop to ensure code style compliance
111
+ 5. Verify all tests pass
112
+ 6. Update documentation if needed
113
+
114
+ ### Before Committing
115
+ ```sh
116
+ # Run linter (must pass)
117
+ bundle exec rubocop
118
+
119
+ # Run tests (must all pass)
120
+ bundle exec rspec
121
+
122
+ # Check test coverage
123
+ bundle exec rspec --format documentation
124
+ ```
125
+
126
+ ### Commit Standards
127
+ - Never commit failing tests
128
+ - Mark unimplemented tests as `skip` or `pending`
129
+ - Use descriptive commit messages
130
+ - Keep commits focused and atomic
131
+
132
+ ## Schema Implementation Notes
133
+
134
+ ### Enum Handling
135
+ - Validate all enum values against XSD schema
136
+ - Provide constants for valid enum values
137
+ - Return original string values, not symbols
138
+
139
+ ### Multi-value Fields
140
+ - Handle comma-separated values in fields like Genre, Characters
141
+ - Provide both string and array access methods
142
+ - Preserve original formatting when possible
143
+
144
+ ### Type Coercion
145
+ - String fields: preserve as-is from XML
146
+ - Integer fields: convert with proper error handling
147
+ - Decimal fields: use BigDecimal for precision
148
+ - Boolean attributes: handle "true"/"false" strings
149
+
150
+ ### Default Values
151
+ - Follow XSD schema default values
152
+ - Empty strings for string fields
153
+ - -1 for unset integer fields
154
+ - "Unknown" for enum fields where applicable
155
+
156
+ ## Testing Strategy
157
+
158
+ ### Fixture Files
159
+ Create comprehensive XML fixtures covering:
160
+ - Minimal valid ComicInfo.xml (required fields only)
161
+ - Complete ComicInfo.xml (all fields populated)
162
+ - Invalid XML (malformed syntax)
163
+ - Invalid data (enum violations, out-of-range values)
164
+ - Edge cases (special characters, Unicode, empty elements)
165
+
166
+ ### Test Categories
167
+ 1. **Unit tests**: Individual method behavior
168
+ 2. **Integration tests**: Full XML parsing workflows
169
+ 3. **Error handling tests**: Invalid inputs and edge cases
170
+ 4. **Fixture helpers**: Centralized test data management
171
+
172
+ ### Test Organization
173
+ ```ruby
174
+ RSpec.describe ComicInfo::Issue do
175
+ describe '.load' do
176
+ context 'with valid file path' do
177
+ it 'loads from fixture' do
178
+ comic = load_fixture 'valid_minimal.xml'
179
+
180
+ expect(comic).to be_a described_class
181
+
182
+ expect(comic.title).to eq 'Expected Title'
183
+ expect(comic.series).to eq 'Expected Series'
184
+ expect(comic.number).to eq '1'
185
+ end
186
+ end
187
+
188
+ context 'with XML string' do
189
+ it 'parses XML content' do
190
+ xml_content = fixture_file 'valid_complete.xml'
191
+ comic = described_class.new xml_content
192
+
193
+ expect(comic).to be_a described_class
194
+ expect(comic.series).to eq 'Expected Series'
195
+ end
196
+ end
197
+ end
198
+
199
+ describe 'multi-value fields' do
200
+ describe 'singular methods (return strings)' do
201
+ it 'returns character as comma-separated string' do
202
+ expect(complete_comic.character).to eq 'Spider-Man, Peter Parker'
203
+ end
204
+ end
205
+
206
+ describe 'plural methods (return arrays)' do
207
+ it 'returns characters as array' do
208
+ expect(complete_comic.characters).to eq ['Spider-Man', 'Peter Parker']
209
+ end
210
+ end
211
+ end
212
+ end
213
+ ```
214
+
215
+ ## Performance Considerations
216
+ - Use Nokogiri’s XML parsing with CSS selectors
217
+ - ComicInfo.xml files are typically small (<100KB)
218
+ - Focus on code clarity over micro-optimizations
219
+
220
+ ## Code Quality Standards
221
+
222
+ ### Test Code Formatting
223
+ - **Alignment Scope**: Only align similar parts of lines within the same method or `it` block
224
+ - **Good Example**: Align multiple expect statements within one test case
225
+ - **Bad Example**: Aligning across different `it` blocks or test methods
226
+ - **Consistency**: Maintain consistent alignment patterns within each test scope
227
+
228
+ ### Test Coverage Requirements
229
+ - All public methods must have corresponding test cases
230
+ - Edge cases and error conditions must be tested
231
+ - Fixture files should cover various scenarios (minimal, complete, invalid, edge cases)
232
+ - Test descriptions should be clear and specific
233
+
234
+ ## Documentation
235
+ - Maintain comprehensive README with examples
236
+ - Document all public methods with YARD comments
237
+ - Include schema field descriptions in code comments
238
+ - Support multiple schema versions (1.0, 2.0, 2.1-draft)
239
+
240
+ ## Implementation Status
241
+ - ✅ Core reading functionality complete
242
+ - ✅ All schema fields implemented and tested
243
+ - ✅ Comprehensive test suite (156 test cases)
244
+ - ✅ Error handling with custom exception classes
245
+ - ✅ Multi-value field support (singular/plural methods)
246
+ - ✅ Page objects with full attribute support
247
+ - ✅ Enum validation and type coercion
248
+ - ✅ Unicode and special character handling
249
+
250
+ ## Future Considerations
251
+ - XML writing/generation capabilities (.to_xml method)
252
+ - JSON export (.to_json method) - **Partially implemented**
253
+ - YAML export (.to_yaml method) - **Partially implemented**
254
+ - Schema version detection and migration
255
+ - CLI tool for ComicInfo manipulation
@@ -0,0 +1,169 @@
1
+ module ComicInfo
2
+ module Enums
3
+ # YesNo enum values for BlackAndWhite field
4
+ YES_NO_VALUES = %w[Unknown No Yes].freeze
5
+
6
+ # Manga enum values including right-to-left reading direction
7
+ MANGA_VALUES = %w[Unknown No Yes YesAndRightToLeft].freeze
8
+
9
+ # Age rating enum values from ComicInfo schema
10
+ AGE_RATING_VALUES = [
11
+ 'Unknown',
12
+ 'Adults Only 18+',
13
+ 'Early Childhood',
14
+ 'Everyone',
15
+ 'Everyone 10+',
16
+ 'G',
17
+ 'Kids to Adults',
18
+ 'M',
19
+ 'MA15+',
20
+ 'Mature 17+',
21
+ 'PG',
22
+ 'R18+',
23
+ 'Rating Pending',
24
+ 'Teen',
25
+ 'X18+'
26
+ ].freeze
27
+
28
+ # Comic page type enum values
29
+ COMIC_PAGE_TYPE_VALUES = %w[
30
+ FrontCover
31
+ InnerCover
32
+ Roundup
33
+ Story
34
+ Advertisement
35
+ Editorial
36
+ Letters
37
+ Preview
38
+ BackCover
39
+ Other
40
+ Deleted
41
+ ].freeze
42
+
43
+ # Default values from XSD schema
44
+ DEFAULT_STRING = ''.freeze
45
+ DEFAULT_INTEGER = -1
46
+ DEFAULT_ENUM_UNKNOWN = 'Unknown'.freeze
47
+ DEFAULT_PAGE_COUNT = 0
48
+ DEFAULT_PAGE_TYPE = 'Story'.freeze
49
+ DEFAULT_DOUBLE_PAGE = false
50
+ DEFAULT_IMAGE_SIZE = 0
51
+
52
+ # Validation methods
53
+ module Validators
54
+ # Validates YesNo enum values
55
+ def self.validate_yes_no value
56
+ return DEFAULT_ENUM_UNKNOWN if value.nil? || value.empty?
57
+
58
+ raise Errors::InvalidEnumError.new('BlackAndWhite', value, YES_NO_VALUES) unless YES_NO_VALUES.include?(value)
59
+
60
+ value
61
+ end
62
+
63
+ # Validates Manga enum values
64
+ def self.validate_manga value
65
+ return DEFAULT_ENUM_UNKNOWN if value.nil? || value.empty?
66
+
67
+ raise Errors::InvalidEnumError.new('Manga', value, MANGA_VALUES) unless MANGA_VALUES.include?(value)
68
+
69
+ value
70
+ end
71
+
72
+ # Validates AgeRating enum values
73
+ def self.validate_age_rating value
74
+ return DEFAULT_ENUM_UNKNOWN if value.nil? || value.empty?
75
+
76
+ raise Errors::InvalidEnumError.new('AgeRating', value, AGE_RATING_VALUES) unless AGE_RATING_VALUES.include?(value)
77
+
78
+ value
79
+ end
80
+
81
+ # Validates ComicPageType enum values
82
+ def self.validate_comic_page_type value
83
+ return DEFAULT_PAGE_TYPE if value.nil? || value.empty?
84
+
85
+ # Handle space-separated list of values
86
+ values = value.split(/\s+/)
87
+ values.each do |v|
88
+ unless COMIC_PAGE_TYPE_VALUES.include?(v)
89
+ raise Errors::InvalidEnumError.new('ComicPageType', v, COMIC_PAGE_TYPE_VALUES)
90
+ end
91
+ end
92
+
93
+ value
94
+ end
95
+
96
+ # Validates CommunityRating decimal range (0.0 to 5.0)
97
+ def self.validate_community_rating value
98
+ return nil if value.nil? || value.empty?
99
+
100
+ begin
101
+ rating = Float(value)
102
+ raise Errors::RangeError.new('CommunityRating', rating, 0.0, 5.0) if rating < 0.0 || rating > 5.0
103
+
104
+ rating
105
+ rescue ArgumentError
106
+ raise Errors::TypeCoercionError.new('CommunityRating', value, 'Float')
107
+ end
108
+ end
109
+
110
+ # Validates integer fields with range checking
111
+ def self.validate_integer value, field_name = 'integer', default = DEFAULT_INTEGER
112
+ return default if value.nil? || value.empty?
113
+
114
+ begin
115
+ Integer(value)
116
+ rescue ArgumentError
117
+ raise Errors::TypeCoercionError.new(field_name, value, 'Integer')
118
+ end
119
+ end
120
+
121
+ # Validates date components (Year, Month, Day)
122
+ def self.validate_year value
123
+ return DEFAULT_INTEGER if value.nil? || value.empty?
124
+
125
+ year = validate_integer(value, 'Year')
126
+ raise Errors::RangeError.new('Year', year, 1000, 9999) if year != DEFAULT_INTEGER && (year < 1000 || year > 9999)
127
+
128
+ year
129
+ end
130
+
131
+ def self.validate_month value
132
+ return DEFAULT_INTEGER if value.nil? || value.empty?
133
+
134
+ month = validate_integer(value, 'Month')
135
+ raise Errors::RangeError.new('Month', month, 1, 12) if month != DEFAULT_INTEGER && (month < 1 || month > 12)
136
+
137
+ month
138
+ end
139
+
140
+ def self.validate_day value
141
+ return DEFAULT_INTEGER if value.nil? || value.empty?
142
+
143
+ day = validate_integer(value, 'Day')
144
+ raise Errors::RangeError.new('Day', day, 1, 31) if day != DEFAULT_INTEGER && (day < 1 || day > 31)
145
+
146
+ day
147
+ end
148
+ end
149
+
150
+ # Helper methods for enum checks
151
+ module Helpers
152
+ def self.manga_right_to_left? value
153
+ value == 'YesAndRightToLeft'
154
+ end
155
+
156
+ def self.yes_value? value
157
+ %w[Yes YesAndRightToLeft].include?(value)
158
+ end
159
+
160
+ def self.no_value? value
161
+ value == 'No'
162
+ end
163
+
164
+ def self.unknown_value? value
165
+ value == 'Unknown' || value.nil? || value.empty?
166
+ end
167
+ end
168
+ end
169
+ end
@@ -0,0 +1,64 @@
1
+ module ComicInfo
2
+ module Errors
3
+ # Base error class for all ComicInfo-related errors
4
+ class ComicInfoError < StandardError; end
5
+
6
+ # Error raised when XML parsing fails
7
+ class ParseError < ComicInfoError
8
+ def initialize message = 'Failed to parse XML'
9
+ super
10
+ end
11
+ end
12
+
13
+ # Error raised when file cannot be read
14
+ class FileError < ComicInfoError
15
+ def initialize message = 'Failed to read file'
16
+ super
17
+ end
18
+ end
19
+
20
+ # Error raised when enum values are invalid
21
+ class InvalidEnumError < ComicInfoError
22
+ attr_reader :field, :value, :valid_values
23
+
24
+ def initialize field, value, valid_values
25
+ @field = field
26
+ @value = value
27
+ @valid_values = valid_values
28
+ super("Invalid value '#{value}' for field '#{field}'. Valid values are: #{valid_values.join(', ')}")
29
+ end
30
+ end
31
+
32
+ # Error raised when numeric values are out of range
33
+ class RangeError < ComicInfoError
34
+ attr_reader :field, :value, :min, :max
35
+
36
+ def initialize field, value, min, max
37
+ @field = field
38
+ @value = value
39
+ @min = min
40
+ @max = max
41
+ super("Value '#{value}' for field '#{field}' is out of range (#{min}..#{max})")
42
+ end
43
+ end
44
+
45
+ # Error raised when type coercion fails
46
+ class TypeCoercionError < ComicInfoError
47
+ attr_reader :field, :value, :expected_type
48
+
49
+ def initialize field, value, expected_type
50
+ @field = field
51
+ @value = value
52
+ @expected_type = expected_type
53
+ super("Cannot convert value '#{value}' for field '#{field}' to #{expected_type}")
54
+ end
55
+ end
56
+
57
+ # Error raised when schema validation fails
58
+ class SchemaError < ComicInfoError
59
+ def initialize message = 'Schema validation failed'
60
+ super
61
+ end
62
+ end
63
+ end
64
+ end