schema-model 0.6.11 → 0.7.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0604e96e9d70ef3b47b7581ef0c6c43005960af46dde776565700abf86482ad5
4
- data.tar.gz: cdfdbf20d77cfd81521df44308083b1134062c19e041752a2a194ee05c420c4d
3
+ metadata.gz: 53c13ff970f92e81b387ef19cb957afed64e77ec42ff63998a2d73fd2c383849
4
+ data.tar.gz: fe073187e725a642bc9e38a43798d99c5388bfad239e709c6d0d8be1c8cf699a
5
5
  SHA512:
6
- metadata.gz: 590a840b90865bc31c4c75c0f34f10135c9d410532c6585d64afa20e39f712262bb43794cea9414877eb8df3602567cf057deb75867d9436eea0315a313aea15
7
- data.tar.gz: 0d9b8ee1ba799df269f396c68692e9210f90bde87192f3f121809a42db99647630b12c5624ed108ff58079a661a1f51b6547e876797dc32a0a887ebb7bcbfc41
6
+ metadata.gz: 8186811e01c702fd082948daccc4ec5f0aaf67afefe15c0ca4200557e0b7397178eb0dcdb4ad1c44500d7284b8da31d6f607f4ae8f5652b3bff3fd66512cc0cd
7
+ data.tar.gz: 4d8b2fc23e68a4209571ce131b893dfa250ad85cf61f37fae48a47098d1fb4cda6730171c32a3c86dfb406f55cafea10a28ff3b65b8b1e33870801c4cfc39c3d
@@ -0,0 +1,93 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [master]
6
+ # release-please PRs change only these files. Their runs are triggered by
7
+ # github-actions[bot], which GitHub holds for approval until they expire as
8
+ # failures; release.yml runs this workflow before publishing anyway. A PR is
9
+ # skipped only if every file it changes is listed here.
10
+ pull_request:
11
+ paths-ignore:
12
+ - CHANGELOG.md
13
+ - .release-please-manifest.json
14
+ - lib/schema/version.rb
15
+ workflow_call:
16
+
17
+ permissions:
18
+ contents: read
19
+
20
+ concurrency:
21
+ group: ci-${{ github.workflow }}-${{ github.ref }}
22
+ cancel-in-progress: ${{ github.event_name == 'pull_request' }}
23
+
24
+ jobs:
25
+ lint:
26
+ name: RuboCop
27
+ runs-on: ubuntu-latest
28
+ steps:
29
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
30
+ with:
31
+ persist-credentials: false
32
+ - uses: ruby/setup-ruby@14594264cd68ce8a2345dd349bc3d138a4ef85c8 # v1.327.0
33
+ with:
34
+ ruby-version: '4.0'
35
+ bundler-cache: true
36
+ - run: bundle exec rubocop --format github
37
+
38
+ # Runs on the Ruby in .ruby-version, the version Gemfile.lock is resolved against
39
+ test:
40
+ name: Tests
41
+ runs-on: ubuntu-latest
42
+ steps:
43
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
44
+ with:
45
+ persist-credentials: false
46
+ - uses: ruby/setup-ruby@14594264cd68ce8a2345dd349bc3d138a4ef85c8 # v1.327.0
47
+ with:
48
+ # no ruby-version: setup-ruby reads .ruby-version
49
+ bundler-cache: true
50
+ - run: bundle exec rspec
51
+ # feeds the coverage badge
52
+ - name: Upload coverage report
53
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
54
+ with:
55
+ name: coverage
56
+ path: coverage/
57
+ # .last_run.json (read by the badge job) is a dotfile; upload-artifact skips those by default
58
+ include-hidden-files: true
59
+ retention-days: 14
60
+
61
+ # Publishes coverage.svg and branches.svg to the orphan `badges` branch; the README badges point at them.
62
+ # Skipped when called from release.yml (github.workflow is the caller's name there).
63
+ coverage-badge:
64
+ if: github.workflow == 'CI' && github.event_name == 'push' && github.ref == 'refs/heads/master'
65
+ needs: test
66
+ runs-on: ubuntu-latest
67
+ permissions:
68
+ contents: write
69
+ steps:
70
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
71
+ - uses: ruby/setup-ruby@14594264cd68ce8a2345dd349bc3d138a4ef85c8 # v1.327.0
72
+ with:
73
+ ruby-version: '4.0'
74
+ - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
75
+ with:
76
+ name: coverage
77
+ path: coverage/
78
+ - run: |
79
+ ruby script/coverage_badge.rb coverage/.last_run.json "$RUNNER_TEMP/coverage.svg"
80
+ ruby script/coverage_badge.rb coverage/.last_run.json "$RUNNER_TEMP/branches.svg" branch
81
+ - name: Push badge to badges branch
82
+ run: |
83
+ git config user.name 'github-actions[bot]'
84
+ git config user.email '41898282+github-actions[bot]@users.noreply.github.com'
85
+ if git fetch --depth=1 origin badges; then
86
+ git switch badges
87
+ else
88
+ git switch --orphan badges
89
+ fi
90
+ cp "$RUNNER_TEMP/coverage.svg" "$RUNNER_TEMP/branches.svg" .
91
+ git add coverage.svg branches.svg
92
+ git diff --cached --quiet || git commit -m "coverage badge for ${GITHUB_SHA::7}"
93
+ git push origin badges
@@ -0,0 +1,93 @@
1
+ name: Release
2
+
3
+ # release-please keeps a release PR open that bumps lib/schema/version.rb
4
+ # and CHANGELOG.md from conventional commits. Merging it tags the release,
5
+ # creates the GitHub release and publishes the gem to RubyGems.
6
+ # Run it manually (workflow_dispatch) to retry publishing the current version.
7
+ on:
8
+ push:
9
+ branches: [master]
10
+ workflow_dispatch:
11
+
12
+ permissions:
13
+ contents: read
14
+
15
+ concurrency:
16
+ group: release
17
+ cancel-in-progress: false
18
+
19
+ jobs:
20
+ release-please:
21
+ runs-on: ubuntu-latest
22
+ permissions:
23
+ contents: write # release commits, tags and GitHub releases
24
+ issues: write # release labels
25
+ pull-requests: write # the release PR
26
+ outputs:
27
+ release_created: ${{ steps.release.outputs.release_created }}
28
+ tag_name: ${{ steps.release.outputs.tag_name }}
29
+ version: ${{ steps.release.outputs.version }}
30
+ pr_branch: ${{ steps.release.outputs.pr && fromJSON(steps.release.outputs.pr).headBranchName || '' }}
31
+ steps:
32
+ - uses: googleapis/release-please-action@45996ed1f6d02564a971a2fa1b5860e934307cf7 # v5.0.0
33
+ id: release
34
+ with:
35
+ config-file: release-please-config.json
36
+ manifest-file: .release-please-manifest.json
37
+
38
+ # The release PR is opened with GITHUB_TOKEN, which doesn't trigger CI on it,
39
+ # so run the suite here before anything is published.
40
+ ci:
41
+ needs: release-please
42
+ if: needs.release-please.outputs.release_created == 'true' || github.event_name == 'workflow_dispatch'
43
+ uses: ./.github/workflows/ci.yml
44
+ # ceiling for the called workflow: its coverage-badge job declares contents: write
45
+ # (skipped when called from here); lint/test still run with ci.yml's top-level contents: read
46
+ permissions:
47
+ contents: write
48
+
49
+ publish:
50
+ needs: [release-please, ci]
51
+ runs-on: ubuntu-latest
52
+ environment:
53
+ name: rubygems
54
+ url: https://rubygems.org/gems/schema-model
55
+ permissions:
56
+ contents: write # attach the gem to the GitHub release
57
+ id-token: write # RubyGems trusted publishing (OIDC)
58
+ steps:
59
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
60
+ with:
61
+ # the new tag on release; the dispatched commit on a manual retry
62
+ ref: ${{ needs.release-please.outputs.tag_name || github.sha }}
63
+ persist-credentials: false
64
+ - uses: ruby/setup-ruby@14594264cd68ce8a2345dd349bc3d138a4ef85c8 # v1.327.0
65
+ with:
66
+ ruby-version: '4.0'
67
+
68
+ - name: Read version
69
+ run: |
70
+ version=$(ruby -e 'puts Gem::Specification.load("schema-model.gemspec").version')
71
+ echo "VERSION=$version" >> "$GITHUB_ENV"
72
+ echo "TAG=v$version" >> "$GITHUB_ENV"
73
+
74
+ - name: Build gem
75
+ run: gem build schema-model.gemspec
76
+
77
+ - uses: rubygems/configure-rubygems-credentials@dc5a8d8553e6ee01fc26761a49e99e733d17954a # v2.1.0
78
+
79
+ # A version that's already on RubyGems is skipped, so a manual retry can finish a
80
+ # partially published release.
81
+ - name: Push gem
82
+ run: |
83
+ if curl -sf "https://rubygems.org/api/v2/rubygems/schema-model/versions/$VERSION.json" > /dev/null; then
84
+ echo "schema-model $VERSION is already published"
85
+ else
86
+ gem push "schema-model-$VERSION.gem"
87
+ fi
88
+
89
+ - name: Attach gem to GitHub release
90
+ env:
91
+ GH_TOKEN: ${{ github.token }}
92
+ run: |
93
+ gh release upload "$TAG" "schema-model-$VERSION.gem" --repo "$GITHUB_REPOSITORY" --clobber
@@ -0,0 +1,3 @@
1
+ {
2
+ ".": "0.7.1"
3
+ }
data/.rubocop.yml CHANGED
@@ -1,11 +1,73 @@
1
+ AllCops:
2
+ TargetRubyVersion: 3.2
3
+ NewCops: enable
4
+ SuggestExtensions: false
5
+ Exclude:
6
+ - 'bin/**/*'
7
+ - 'vendor/**/*'
8
+ - 'coverage/**/*'
9
+
10
+ # Relaxed metrics for existing codebase
11
+ Metrics/MethodLength:
12
+ Max: 35
13
+
14
+ Metrics/AbcSize:
15
+ Max: 40
16
+
17
+ Metrics/ClassLength:
18
+ Max: 200
19
+
20
+ Metrics/ModuleLength:
21
+ Max: 200
22
+
23
+ Metrics/CyclomaticComplexity:
24
+ Max: 15
25
+
26
+ Metrics/PerceivedComplexity:
27
+ Max: 15
28
+
29
+ Metrics/BlockLength:
30
+ Exclude:
31
+ - 'spec/**/*'
32
+ - '*.gemspec'
33
+
34
+ # Style preferences
35
+ Style/Documentation:
36
+ Enabled: false
37
+
38
+ Style/FrozenStringLiteralComment:
39
+ EnforcedStyle: always
40
+
41
+ Layout/LineLength:
42
+ Max: 130
43
+ Exclude:
44
+ - 'spec/**/*'
45
+
46
+ # File naming - allow hyphenated gem name
1
47
  Naming/FileName:
48
+ Exclude:
49
+ - 'lib/schema-model.rb'
50
+
51
+ # Gemspec settings
52
+ Gemspec/RequiredRubyVersion:
2
53
  Enabled: false
3
- Metrics/LineLength:
4
- Max: 120
5
- Metrics/ModuleLength:
6
- Max: 120
7
- Metrics/MethodLength:
8
- Max: 20
9
- AllCops:
54
+
55
+ # DSL methods following ActiveRecord conventions (has_one, has_many)
56
+ Naming/PredicatePrefix:
10
57
  Exclude:
11
- - 'spec/**/*_spec.rb'
58
+ - 'lib/schema/associations/has_one.rb'
59
+ - 'lib/schema/associations/has_many.rb'
60
+ - 'lib/schema/associations/schema_creator.rb'
61
+
62
+ # Dynamic eval is used for metaprogramming in this DSL gem
63
+ Style/DocumentDynamicEvalDefinition:
64
+ Enabled: false
65
+
66
+ # Short parameter names acceptable in simple setters
67
+ Naming/MethodParameterName:
68
+ AllowedNames:
69
+ - v
70
+
71
+ # Boolean parameters acceptable in internal methods
72
+ Style/OptionalBooleanParameter:
73
+ Enabled: false
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 3.4.2
1
+ ruby-4.0.7
data/ARCHITECTURE.md ADDED
@@ -0,0 +1,135 @@
1
+ # Architecture
2
+
3
+ This document describes the internal architecture of the `schema-model` gem.
4
+
5
+ ## Overview
6
+
7
+ The gem transforms hash data into strongly-typed Ruby objects with parsing, validation, and nested associations. The core flow is:
8
+
9
+ ```
10
+ Hash Data → from_hash() → Parser Methods → Schema Instance
11
+ ↓
12
+ parsing_errors (if invalid)
13
+ ```
14
+
15
+ ## Module Hierarchy
16
+
17
+ ```
18
+ Schema::All (convenience bundle)
19
+ ├── Schema::Model (core attribute system)
20
+ ├── Schema::Associations::HasOne
21
+ ├── Schema::Associations::HasMany
22
+ ├── Schema::Parsers::Common
23
+ ├── Schema::Parsers::American
24
+ ├── Schema::Parsers::Array
25
+ ├── Schema::Parsers::Hash
26
+ ├── Schema::Parsers::Json
27
+ └── Schema::ActiveModelValidations
28
+ ```
29
+
30
+ ## Core Components
31
+
32
+ ### Schema::Model (`lib/schema/model.rb`)
33
+
34
+ The foundation module providing:
35
+
36
+ - **`attribute(name, type, options)`**: Defines schema fields. Each call:
37
+ 1. Adds field metadata to the class's `schema` hash via `add_value_to_class_method`
38
+ 2. Generates getter, setter, and `<name>_was_set?` methods
39
+ 3. Setter invokes `parse_<type>` method automatically
40
+
41
+ - **`from_hash(data, skip_fields)`**: Class method that creates instance and calls `update_attributes`
42
+
43
+ - **`update_attributes(data, skip_fields)`**: Iterates hash keys, matches against schema, invokes setters
44
+
45
+ - **`as_json` / `to_hash`**: Serialization back to hash format
46
+
47
+ ### Schema::Parsers::Common (`lib/schema/parsers/common.rb`)
48
+
49
+ Base parser methods for fundamental types:
50
+ - `parse_integer`, `parse_float`, `parse_string`, `parse_string_or_nil`
51
+ - `parse_boolean`, `parse_time`, `parse_date`
52
+
53
+ Each parser:
54
+ 1. Accepts `(field_name, parsing_errors, value)`
55
+ 2. Returns converted value or nil
56
+ 3. Adds to `parsing_errors` on failure (never raises)
57
+
58
+ Additional parsers extend these: `Parsers::American` (date formats), `Parsers::Array`, `Parsers::Hash`, `Parsers::Json`.
59
+
60
+ ### Schema::Associations (`lib/schema/associations/`)
61
+
62
+ **HasOne** and **HasMany** define nested relationships:
63
+
64
+ ```ruby
65
+ has_one(:profile) { attribute :bio, :string }
66
+ has_many(:posts) { attribute :title, :string }
67
+ ```
68
+
69
+ Both use **SchemaCreator** (`schema_creator.rb`) to:
70
+ 1. Determine which class to instantiate (static or dynamic)
71
+ 2. Call `from_hash` on the nested class
72
+ 3. Propagate parsing errors to parent
73
+
74
+ **DynamicTypes** enables polymorphic associations:
75
+
76
+ ```ruby
77
+ has_many(:items, type_field: :kind) do
78
+ add_type('widget') { attribute :size, :integer }
79
+ add_type('gadget') { attribute :power, :float }
80
+ default_type { } # fallback
81
+ end
82
+ ```
83
+
84
+ The `type_field` option tells SchemaCreator which data key determines the subclass.
85
+
86
+ ### Schema::Utils (`lib/schema/utils.rb`)
87
+
88
+ Utility methods for:
89
+ - `classify_name`: String → ClassName conversion
90
+ - `create_schema_class`: Dynamically creates nested schema classes
91
+ - `add_association_class`: Wires up association with proper modules
92
+ - `add_attribute_default_methods` / `add_association_default_methods`: Default value handling
93
+
94
+ ### Error Handling
95
+
96
+ Two error storage mechanisms:
97
+
98
+ 1. **Schema::Errors** (`lib/schema/errors.rb`): Simple hash-based storage, used standalone
99
+
100
+ 2. **ActiveModel::Errors**: When `Schema::ActiveModelValidations` is included, `parsing_errors` returns `ActiveModel::Errors` instance
101
+
102
+ Parsing errors are distinct from validation errors:
103
+ - **Parsing errors**: Type conversion failures (string "abc" → integer)
104
+ - **Validation errors**: Business rule failures (via `validates` DSL)
105
+
106
+ Methods `parsed!` and `valid!` raise `ParsingException`/`ValidationException` respectively.
107
+
108
+ ### Inheritance Helper Integration
109
+
110
+ The gem uses `inheritance-helper` for schema inheritance. Key method:
111
+ - `add_value_to_class_method(:schema, name => options)`: Accumulates schema definitions across class hierarchy
112
+
113
+ This allows schema classes to inherit attributes from parent classes.
114
+
115
+ ## Data Flow Example
116
+
117
+ ```ruby
118
+ class OrderSchema
119
+ include Schema::All
120
+ attribute :total, :float
121
+ has_one(:customer) { attribute :name, :string }
122
+ end
123
+
124
+ order = OrderSchema.from_hash({
125
+ total: "99.50",
126
+ customer: { name: "Alice" }
127
+ })
128
+ ```
129
+
130
+ 1. `from_hash` calls `new.update_attributes(data)`
131
+ 2. `update_attributes` iterates keys, finds `:total` in schema
132
+ 3. Calls `self.total = "99.50"` → invokes `parse_float`
133
+ 4. For `:customer`, recognizes association, delegates to SchemaCreator
134
+ 5. SchemaCreator calls `OrderSchema::SchemaHasOneCustomer.from_hash({name: "Alice"})`
135
+ 6. Returns populated `OrderSchema` instance
data/CHANGELOG.md ADDED
@@ -0,0 +1,9 @@
1
+ # Changelog
2
+
3
+ ## [0.7.1](https://github.com/dougyouch/schema/compare/v0.7.0...v0.7.1) (2026-10-04)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * **specs:** replace deprecated SimpleCov API calls ([dadae4a](https://github.com/dougyouch/schema/commit/dadae4aa2d0246e964fdb160e1b505187e3fadc6))
9
+ * **specs:** wrap trailing hash literals in braces per Style/HashAsLastArrayItem ([cf8f18a](https://github.com/dougyouch/schema/commit/cf8f18af5b7c472298c7c0cbd7b6176356b476fc))
data/CLAUDE.md ADDED
@@ -0,0 +1,83 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## Build and Test Commands
6
+
7
+ ```bash
8
+ # Install dependencies
9
+ bundle install
10
+
11
+ # Run all tests
12
+ bundle exec rspec
13
+
14
+ # Run a single test file
15
+ bundle exec rspec spec/schema/model_spec.rb
16
+
17
+ # Run a specific test by line number
18
+ bundle exec rspec spec/schema/model_spec.rb:42
19
+
20
+ # Run linter
21
+ bundle exec rubocop
22
+
23
+ # Auto-fix linter issues
24
+ bundle exec rubocop -A
25
+ ```
26
+
27
+ ## Architecture
28
+
29
+ This is a Ruby gem (`schema-model`) for data transformation, validation, and type safety. It transforms hash data into strongly-typed model objects.
30
+
31
+ ### Core Module Structure
32
+
33
+ - **Schema::Model** (`lib/schema/model.rb`) - Foundation module providing `attribute` definitions, `from_hash` class method, and attribute accessors.
34
+
35
+ - **Schema::All** (`lib/schema/all.rb`) - Convenience module bundling Model + Associations + Parsers + ActiveModel validations. This is the typical include.
36
+
37
+ - **Schema::Parsers** - Type parsers in `lib/schema/parsers/`:
38
+ - `Common` - integer, string, float, time, date, boolean
39
+ - `American` - american_date, american_time (MM/DD/YYYY format)
40
+ - `Array` - array with optional separator and data_type
41
+ - `Hash` - hash/dictionary values
42
+ - `Json` - JSON string parsing
43
+
44
+ - **Schema::Associations** - `HasOne` and `HasMany` for nested relationships. `DynamicTypes` enables polymorphic associations via `type_field`/`add_type`.
45
+
46
+ - **Schema::Arrays** (`lib/schema/arrays.rb`) - Convert models to/from flat arrays for CSV support.
47
+
48
+ - **Schema::ArrayHeaders** (`lib/schema/array_headers.rb`) - Map CSV headers to schema attributes.
49
+
50
+ ### Key Patterns
51
+
52
+ **Attribute Definition**: Each `attribute` call generates getter, setter, and `<name>_was_set?` predicate. Setter invokes type-specific parser.
53
+
54
+ **Parsing Errors**: Stored in `parsing_errors`. Parsers add errors for invalid values rather than raising exceptions. With ActiveModelValidations, use `parsed?`/`parsed!`.
55
+
56
+ **Schema Inheritance**: Uses `inheritance-helper` gem. Schema definitions accumulate via `add_value_to_class_method(:schema, ...)`.
57
+
58
+ **Dynamic Types**: For polymorphic associations, use `type_field` option with `add_type` blocks. Supports `external_type_field`, `type_ignorecase`, and `default_type`.
59
+
60
+ ## CI
61
+
62
+ `.github/workflows/ci.yml` runs RuboCop and the specs on the `.ruby-version` Ruby. On pushes to `master`, it publishes `coverage.svg` (line) and `branches.svg` (branch) from `script/coverage_badge.rb` to the orphan `badges` branch for the README badges.
63
+
64
+ ## Releases
65
+
66
+ Releases are automated by release-please (`.github/workflows/release.yml`). Conventional commits on `master` (`fix:` → patch, `feat:` → minor, `!`/`BREAKING CHANGE` → major) update an open release PR that bumps `lib/schema/version.rb` (`Schema::VERSION`, read by the gemspec) and `CHANGELOG.md`. Merging that PR tags `vX.Y.Z`, creates the GitHub release, and publishes the gem to RubyGems. Don't bump the version by hand.
67
+
68
+ ## Code Commits
69
+
70
+ Format using angular formatting:
71
+ ```
72
+ <type>(<scope>): <short summary>
73
+ ```
74
+ - **type**: build|ci|docs|feat|fix|perf|refactor|test
75
+ - **scope**: The feature or component of the service we're working on
76
+ - **summary**: Summary in present tense. Not capitalized. No period at the end.
77
+
78
+ ## Documentation Maintenance
79
+
80
+ When modifying the codebase, keep documentation in sync:
81
+ - **ARCHITECTURE.md** - Update when adding/removing classes, changing component relationships, or altering data flow patterns
82
+ - **README.md** - Update when adding new features, changing public APIs, or modifying usage examples
83
+ - **CHANGELOG.md** - Generated by release-please from commit messages; don't edit by hand except to fix release notes
data/Gemfile CHANGED
@@ -1,13 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- source 'http://rubygems.org'
3
+ source 'https://rubygems.org'
4
4
 
5
5
  gem 'inheritance-helper'
6
6
 
7
7
  group :development do
8
8
  gem 'activemodel'
9
9
  gem 'csv'
10
- gem 'rake'
11
10
  gem 'rubocop'
12
11
  end
13
12