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.
- checksums.yaml +7 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +41 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/LICENSE.md +21 -0
- data/README.md +295 -0
- data/Rakefile +10 -0
- data/doc/development.md +255 -0
- data/lib/comicinfo/enums.rb +169 -0
- data/lib/comicinfo/errors.rb +64 -0
- data/lib/comicinfo/issue.rb +332 -0
- data/lib/comicinfo/page.rb +182 -0
- data/lib/comicinfo/version.rb +3 -0
- data/lib/comicinfo.rb +15 -0
- data/schemas/1.0/ComicInfo.xsd +77 -0
- data/schemas/2.0/ComicInfo.xsd +123 -0
- data/schemas/2.1-draft/ComicInfo.xsd +127 -0
- data/script/schema +95 -0
- data/sig/comicinfo.rbs +4 -0
- metadata +79 -0
data/doc/development.md
ADDED
@@ -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
|