schema-model 0.7.4 → 0.9.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,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Schema
4
+ module Parsers
5
+ # Schema::Parsers::StringValue is the shared string handling for parsers
6
+ module StringValue
7
+ # strips the string, returns nil when it's blank, and otherwise yields it; a nil result
8
+ # or an ArgumentError (e.g. Date::Error) from the block is recorded as invalid
9
+ def parse_string_value(field_name, parsing_errors, value)
10
+ str = value.strip
11
+ return nil if str.empty?
12
+
13
+ result = begin
14
+ yield str
15
+ rescue ArgumentError
16
+ nil
17
+ end
18
+ parsing_errors.add(field_name, ::Schema::ParsingErrors::INVALID) if result.nil?
19
+ result
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Schema
4
+ # Schema::ParsingStatus reports whether a model parsed cleanly, and raises at an explicit boundary
5
+ module ParsingStatus
6
+ def parsed?
7
+ parsing_errors.empty?
8
+ end
9
+
10
+ def parsed!
11
+ return if parsed?
12
+
13
+ raise ParsingException.new(
14
+ "schema parsing failed for attributes #{parsing_errors.attribute_names.join(', ')}",
15
+ self,
16
+ parsing_errors
17
+ )
18
+ end
19
+ end
20
+ end
data/lib/schema/utils.rb CHANGED
@@ -5,6 +5,14 @@ module Schema
5
5
  module Utils
6
6
  module_function
7
7
 
8
+ def check_parser!(model, parser, field_name, type)
9
+ return if model.respond_to?(parser)
10
+
11
+ raise ::Schema::UnknownTypeError,
12
+ "unknown type #{type.inspect} for #{model.class.name || model.class}##{field_name}: " \
13
+ "no #{parser} method (schema_include the parser module that defines it)"
14
+ end
15
+
8
16
  def classify_name(name)
9
17
  name.gsub(/[^\da-z_-]/, '').gsub(/(^.|[_|-].)/) { |m| m[-1].upcase }
10
18
  end
@@ -14,6 +22,7 @@ module Schema
14
22
  kls = Class.new(options[:base_class] || Object)
15
23
  kls = base_schema_class.const_set(options[:class_name], kls)
16
24
  include_schema_modules(kls, base_schema_class.schema_config) unless options[:base_class]
25
+ kls.capture_unknown_attributes = base_schema_class.capture_unknown_attributes?
17
26
  kls
18
27
  end
19
28
 
@@ -56,6 +65,24 @@ module Schema
56
65
  kls.send(:include, ::Schema::Associations::DynamicTypes)
57
66
  end
58
67
 
68
+ # copies nested models and collections so the copy can be changed without touching the original
69
+ def deep_dup_value(value)
70
+ case value
71
+ when ::Schema::Model then value.deep_dup
72
+ when ::Array then value.map { |element| deep_dup_value(element) }
73
+ when ::Hash then value.to_h { |key, element| [key, deep_dup_value(element)] }
74
+ when ::String then value.frozen? ? value : value.dup
75
+ else value
76
+ end
77
+ end
78
+
79
+ # [attribute, message] pairs from Schema::Errors or ActiveModel::Errors
80
+ def parsing_error_pairs(errors)
81
+ return errors.map { |error| [error.attribute, error.message] } unless errors.is_a?(::Schema::Errors)
82
+
83
+ errors.errors.flat_map { |attribute, messages| messages.map { |message| [attribute, message] } }
84
+ end
85
+
59
86
  # each call gets its own copy so callers can't mutate the shared default
60
87
  def copy_default(value)
61
88
  return value if value.frozen?
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Schema
4
4
  # Gem version, bumped by release-please
5
- VERSION = '0.7.4'
5
+ VERSION = '0.9.0'
6
6
  end
data/lib/schema-model.rb CHANGED
@@ -18,14 +18,21 @@ module Schema
18
18
  class ParsingException < SchemaException; end
19
19
  class ValidationException < SchemaException; end
20
20
 
21
+ # raised when a value is assigned to an attribute whose type has no parse_<type> method
22
+ class UnknownTypeError < StandardError; end
23
+
21
24
  autoload :ActiveModelValidations, 'schema/active_model_validations'
22
25
  autoload :All, 'schema/all'
26
+ autoload :ActiveModelParsingErrors, 'schema/active_model_parsing_errors'
23
27
  autoload :ArrayHeaders, 'schema/array_headers'
24
28
  autoload :Arrays, 'schema/arrays'
29
+ autoload :AttributeValues, 'schema/attribute_values'
25
30
  autoload :CSVParser, 'schema/csv_parser'
31
+ autoload :DeepCopy, 'schema/deep_copy'
26
32
  autoload :Errors, 'schema/errors'
27
33
  autoload :Model, 'schema/model'
28
34
  autoload :ParsingErrors, 'schema/parsing_errors'
35
+ autoload :ParsingStatus, 'schema/parsing_status'
29
36
  autoload :Utils, 'schema/utils'
30
37
 
31
38
  # Schema::Parsers are used to convert values into the correct data type
@@ -35,6 +42,7 @@ module Schema
35
42
  autoload :Common, 'schema/parsers/common'
36
43
  autoload :Hash, 'schema/parsers/hash'
37
44
  autoload :Json, 'schema/parsers/json'
45
+ autoload :StringValue, 'schema/parsers/string_value'
38
46
  end
39
47
 
40
48
  # Schema::Associations mange the associations between schema models
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # SchemaValidator validates nested schemas
3
+ # SchemaValidator validates nested schemas: each one must have no parsing errors and pass its validations
4
4
  class SchemaValidator < ActiveModel::EachValidator
5
5
  def validate_each(record, attribute, value)
6
6
  record.errors.add(attribute, options.fetch(:message, :invalid)) unless valid_schema?(value)
@@ -11,12 +11,13 @@ class SchemaValidator < ActiveModel::EachValidator
11
11
  def valid_schema?(value)
12
12
  return true unless value
13
13
 
14
- if value.is_a?(Array)
15
- value.all? do |schema|
16
- !schema || schema.valid?
17
- end
18
- else
19
- value.valid?
20
- end
14
+ schemas = value.is_a?(Array) ? value.compact : [value]
15
+ # map before all? so every nested schema runs its validations and has its errors filled in
16
+ schemas.map { |schema| parsed_and_valid?(schema) }.all?
17
+ end
18
+
19
+ def parsed_and_valid?(schema)
20
+ valid = schema.valid?
21
+ valid && schema.parsed?
21
22
  end
22
23
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: schema-model
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.4
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Doug Youch
@@ -34,23 +34,12 @@ executables:
34
34
  extensions: []
35
35
  extra_rdoc_files: []
36
36
  files:
37
- - ".github/dependabot.yml"
38
- - ".github/workflows/ci.yml"
39
- - ".github/workflows/release.yml"
40
- - ".gitignore"
41
- - ".release-please-manifest.json"
42
- - ".rubocop.yml"
43
- - ".ruby-gemset"
44
- - ".ruby-version"
45
- - ARCHITECTURE.md
46
37
  - CHANGELOG.md
47
- - CLAUDE.md
48
- - Gemfile
49
- - Gemfile.lock
50
38
  - LICENSE.txt
51
39
  - README.md
52
40
  - bin/schema-json2csv
53
41
  - lib/schema-model.rb
42
+ - lib/schema/active_model_parsing_errors.rb
54
43
  - lib/schema/active_model_validations.rb
55
44
  - lib/schema/all.rb
56
45
  - lib/schema/array_headers.rb
@@ -60,7 +49,9 @@ files:
60
49
  - lib/schema/associations/has_many.rb
61
50
  - lib/schema/associations/has_one.rb
62
51
  - lib/schema/associations/schema_creator.rb
52
+ - lib/schema/attribute_values.rb
63
53
  - lib/schema/csv_parser.rb
54
+ - lib/schema/deep_copy.rb
64
55
  - lib/schema/errors.rb
65
56
  - lib/schema/model.rb
66
57
  - lib/schema/parsers/american.rb
@@ -68,19 +59,20 @@ files:
68
59
  - lib/schema/parsers/common.rb
69
60
  - lib/schema/parsers/hash.rb
70
61
  - lib/schema/parsers/json.rb
62
+ - lib/schema/parsers/string_value.rb
71
63
  - lib/schema/parsing_errors.rb
64
+ - lib/schema/parsing_status.rb
72
65
  - lib/schema/utils.rb
73
66
  - lib/schema/version.rb
74
67
  - lib/schema_validator.rb
75
- - release-please-config.json
76
- - schema-model.gemspec
77
- - script/console
78
- - script/coverage_badge.rb
79
68
  homepage: https://github.com/dougyouch/schema
80
69
  licenses:
81
70
  - MIT
82
71
  metadata:
83
72
  rubygems_mfa_required: 'true'
73
+ source_code_uri: https://github.com/dougyouch/schema
74
+ changelog_uri: https://github.com/dougyouch/schema/blob/master/CHANGELOG.md
75
+ bug_tracker_uri: https://github.com/dougyouch/schema/issues
84
76
  rdoc_options: []
85
77
  require_paths:
86
78
  - lib
@@ -1,18 +0,0 @@
1
- version: 2
2
- updates:
3
- # keeps the SHA-pinned actions in .github/workflows current
4
- - package-ecosystem: github-actions
5
- directory: /
6
- schedule:
7
- interval: weekly
8
- commit-message:
9
- prefix: ci
10
- include: scope
11
-
12
- - package-ecosystem: bundler
13
- directory: /
14
- schedule:
15
- interval: weekly
16
- commit-message:
17
- prefix: build
18
- include: scope
@@ -1,93 +0,0 @@
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
@@ -1,93 +0,0 @@
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/.gitignore DELETED
@@ -1,51 +0,0 @@
1
- # rcov generated
2
- coverage
3
- coverage.data
4
-
5
- # rdoc generated
6
- rdoc
7
-
8
- # yard generated
9
- doc
10
- .yardoc
11
-
12
- # bundler
13
- .bundle
14
-
15
- # jeweler generated
16
- pkg
17
-
18
- # Have editor/IDE/OS specific files you need to ignore? Consider using a global gitignore:
19
- #
20
- # * Create a file at ~/.gitignore
21
- # * Include files you want ignored
22
- # * Run: git config --global core.excludesfile ~/.gitignore
23
- #
24
- # After doing this, these files will be ignored in all your git projects,
25
- # saving you from having to 'pollute' every project you touch with them
26
- #
27
- # Not sure what to needs to be ignored for particular editors/OSes? Here's some ideas to get you started. (Remember, remove the leading # of the line)
28
- #
29
- # For MacOS:
30
- #
31
- .DS_Store
32
-
33
- # For TextMate
34
- #*.tmproj
35
- #tmtags
36
-
37
- # For emacs:
38
- *~
39
- \#*
40
- .\#*
41
-
42
- # For vim:
43
- *.swp
44
-
45
- # For redcar:
46
- #.redcar
47
-
48
- # For rubinius:
49
- #*.rbc
50
-
51
- *.gem
@@ -1,3 +0,0 @@
1
- {
2
- ".": "0.7.4"
3
- }
data/.rubocop.yml DELETED
@@ -1,68 +0,0 @@
1
- AllCops:
2
- TargetRubyVersion: 3.2
3
- NewCops: enable
4
- SuggestExtensions: false
5
- Exclude:
6
- - 'vendor/**/*'
7
- - 'coverage/**/*'
8
-
9
- # Relaxed metrics for existing codebase
10
- Metrics/MethodLength:
11
- Max: 35
12
-
13
- Metrics/AbcSize:
14
- Max: 40
15
-
16
- Metrics/ClassLength:
17
- Max: 200
18
-
19
- Metrics/ModuleLength:
20
- Max: 200
21
-
22
- Metrics/CyclomaticComplexity:
23
- Max: 15
24
-
25
- Metrics/PerceivedComplexity:
26
- Max: 15
27
-
28
- Metrics/BlockLength:
29
- Exclude:
30
- - 'spec/**/*'
31
- - '*.gemspec'
32
-
33
- # Style preferences
34
- Style/Documentation:
35
- Enabled: false
36
-
37
- Style/FrozenStringLiteralComment:
38
- EnforcedStyle: always
39
-
40
- Layout/LineLength:
41
- Max: 130
42
- Exclude:
43
- - 'spec/**/*'
44
-
45
- # File naming - allow hyphenated gem name
46
- Naming/FileName:
47
- Exclude:
48
- - 'lib/schema-model.rb'
49
-
50
- # DSL methods following ActiveRecord conventions (has_one, has_many)
51
- Naming/PredicatePrefix:
52
- Exclude:
53
- - 'lib/schema/associations/has_one.rb'
54
- - 'lib/schema/associations/has_many.rb'
55
- - 'lib/schema/associations/schema_creator.rb'
56
-
57
- # Dynamic eval is used for metaprogramming in this DSL gem
58
- Style/DocumentDynamicEvalDefinition:
59
- Enabled: false
60
-
61
- # Short parameter names acceptable in simple setters
62
- Naming/MethodParameterName:
63
- AllowedNames:
64
- - v
65
-
66
- # Boolean parameters acceptable in internal methods
67
- Style/OptionalBooleanParameter:
68
- Enabled: false
data/.ruby-gemset DELETED
@@ -1 +0,0 @@
1
- schema
data/.ruby-version DELETED
@@ -1 +0,0 @@
1
- ruby-4.0.7