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 +4 -4
- data/.github/workflows/ci.yml +93 -0
- data/.github/workflows/release.yml +93 -0
- data/.release-please-manifest.json +3 -0
- data/.rubocop.yml +70 -8
- data/.ruby-version +1 -1
- data/ARCHITECTURE.md +135 -0
- data/CHANGELOG.md +9 -0
- data/CLAUDE.md +83 -0
- data/Gemfile +1 -2
- data/Gemfile.lock +79 -47
- data/README.md +361 -85
- data/lib/schema/active_model_validations.rb +14 -14
- data/lib/schema/array_headers.rb +6 -4
- data/lib/schema/arrays.rb +11 -13
- data/lib/schema/associations/dynamic_types.rb +3 -5
- data/lib/schema/associations/has_many.rb +2 -4
- data/lib/schema/associations/has_one.rb +3 -5
- data/lib/schema/associations/schema_creator.rb +2 -2
- data/lib/schema/csv_parser.rb +1 -1
- data/lib/schema/model.rb +26 -21
- data/lib/schema/parsers/array.rb +2 -2
- data/lib/schema/parsers/common.rb +6 -8
- data/lib/schema/parsers/hash.rb +1 -1
- data/lib/schema/parsers/json.rb +1 -1
- data/lib/schema/parsing_errors.rb +1 -1
- data/lib/schema/utils.rb +5 -5
- data/lib/schema/version.rb +6 -0
- data/lib/schema-model.rb +1 -0
- data/release-please-config.json +12 -0
- data/schema-model.gemspec +8 -4
- data/script/coverage_badge.rb +49 -0
- metadata +17 -12
- data/.cursor-rules +0 -39
- data/.travis.yml +0 -14
- data/Rakefile +0 -11
- data/docs/ARCHITECTURE.md +0 -171
- data/script/bundle_install_all_versions +0 -8
- data/script/rspec_all_versions +0 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 53c13ff970f92e81b387ef19cb957afed64e77ec42ff63998a2d73fd2c383849
|
|
4
|
+
data.tar.gz: fe073187e725a642bc9e38a43798d99c5388bfad239e709c6d0d8be1c8cf699a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
- '
|
|
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
|
-
|
|
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
|