schema-model 0.7.3 → 0.8.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.
@@ -6,20 +6,24 @@ module Schema
6
6
  module Parsers
7
7
  # Schema::Parsers::Common are parser methods for basic types
8
8
  module Common
9
- INTEGER_REGEX = /\A[-+]?(?:[1-9]\d*|0)(?:\.0+)?\z/
10
- FLOAT_REGEX = /\A[-+]?(?:[1-9]\d*|0)(?:\.\d+)?([Ee][-+]?\d+)?\z/
11
- BOOLEAN_REGEX = /\A(?:1|t|true|on|y|yes)\z/i
9
+ include StringValue
10
+
11
+ # strings are stripped first; a blank string parses to nil without an error
12
+ INTEGER_REGEX = /\A[-+]?\d+(?:\.0+)?\z/
13
+ FLOAT_REGEX = /\A[-+]?\d+(?:\.\d+)?([Ee][-+]?\d+)?\z/
14
+ # matched case-insensitively; any other string is invalid
15
+ BOOLEAN_STRINGS = {
16
+ '1' => true, 't' => true, 'true' => true, 'on' => true, 'y' => true, 'yes' => true,
17
+ '0' => false, 'f' => false, 'false' => false, 'off' => false, 'n' => false, 'no' => false
18
+ }.freeze
12
19
 
13
20
  def parse_integer(field_name, parsing_errors, value)
14
21
  case value
15
22
  when Integer
16
23
  value
17
24
  when String
18
- if INTEGER_REGEX.match(value)
19
- value.to_i
20
- else
21
- parsing_errors.add(field_name, ::Schema::ParsingErrors::INVALID)
22
- nil
25
+ parse_string_value(field_name, parsing_errors, value) do |str|
26
+ str.to_i if INTEGER_REGEX.match?(str)
23
27
  end
24
28
  when Float
25
29
  parse_float_as_integer(field_name, parsing_errors, value)
@@ -77,11 +81,8 @@ module Schema
77
81
  when Integer
78
82
  value.to_f
79
83
  when String
80
- if FLOAT_REGEX.match(value)
81
- Float(value)
82
- else
83
- parsing_errors.add(field_name, ::Schema::ParsingErrors::INVALID)
84
- nil
84
+ parse_string_value(field_name, parsing_errors, value) do |str|
85
+ Float(str) if FLOAT_REGEX.match?(str)
85
86
  end
86
87
  when nil
87
88
  nil
@@ -98,12 +99,7 @@ module Schema
98
99
  when Date
99
100
  value.to_time
100
101
  when String
101
- begin
102
- Time.xmlschema(value)
103
- rescue ArgumentError
104
- parsing_errors.add(field_name, ::Schema::ParsingErrors::INVALID)
105
- nil
106
- end
102
+ parse_string_value(field_name, parsing_errors, value) { |str| Time.xmlschema(str) }
107
103
  when nil
108
104
  nil
109
105
  else
@@ -119,12 +115,7 @@ module Schema
119
115
  when Time
120
116
  value.to_date
121
117
  when String
122
- begin
123
- Date.parse(value)
124
- rescue ArgumentError
125
- parsing_errors.add(field_name, ::Schema::ParsingErrors::INVALID)
126
- nil
127
- end
118
+ parse_string_value(field_name, parsing_errors, value) { |str| Date.iso8601(str) }
128
119
  when nil
129
120
  nil
130
121
  else
@@ -140,7 +131,7 @@ module Schema
140
131
  when Integer, Float
141
132
  value != 0
142
133
  when String
143
- BOOLEAN_REGEX.match?(value)
134
+ parse_string_value(field_name, parsing_errors, value) { |str| BOOLEAN_STRINGS[str.downcase] }
144
135
  when nil
145
136
  nil
146
137
  else
@@ -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.3'
5
+ VERSION = '0.8.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
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.3
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Doug Youch
@@ -34,22 +34,12 @@ executables:
34
34
  extensions: []
35
35
  extra_rdoc_files: []
36
36
  files:
37
- - ".github/workflows/ci.yml"
38
- - ".github/workflows/release.yml"
39
- - ".gitignore"
40
- - ".release-please-manifest.json"
41
- - ".rubocop.yml"
42
- - ".ruby-gemset"
43
- - ".ruby-version"
44
- - ARCHITECTURE.md
45
37
  - CHANGELOG.md
46
- - CLAUDE.md
47
- - Gemfile
48
- - Gemfile.lock
49
38
  - LICENSE.txt
50
39
  - README.md
51
40
  - bin/schema-json2csv
52
41
  - lib/schema-model.rb
42
+ - lib/schema/active_model_parsing_errors.rb
53
43
  - lib/schema/active_model_validations.rb
54
44
  - lib/schema/all.rb
55
45
  - lib/schema/array_headers.rb
@@ -59,7 +49,9 @@ files:
59
49
  - lib/schema/associations/has_many.rb
60
50
  - lib/schema/associations/has_one.rb
61
51
  - lib/schema/associations/schema_creator.rb
52
+ - lib/schema/attribute_values.rb
62
53
  - lib/schema/csv_parser.rb
54
+ - lib/schema/deep_copy.rb
63
55
  - lib/schema/errors.rb
64
56
  - lib/schema/model.rb
65
57
  - lib/schema/parsers/american.rb
@@ -67,19 +59,20 @@ files:
67
59
  - lib/schema/parsers/common.rb
68
60
  - lib/schema/parsers/hash.rb
69
61
  - lib/schema/parsers/json.rb
62
+ - lib/schema/parsers/string_value.rb
70
63
  - lib/schema/parsing_errors.rb
64
+ - lib/schema/parsing_status.rb
71
65
  - lib/schema/utils.rb
72
66
  - lib/schema/version.rb
73
67
  - lib/schema_validator.rb
74
- - release-please-config.json
75
- - schema-model.gemspec
76
- - script/console
77
- - script/coverage_badge.rb
78
68
  homepage: https://github.com/dougyouch/schema
79
69
  licenses:
80
70
  - MIT
81
71
  metadata:
82
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
83
76
  rdoc_options: []
84
77
  require_paths:
85
78
  - lib
@@ -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.3"
3
- }
data/.rubocop.yml DELETED
@@ -1,69 +0,0 @@
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
47
- Naming/FileName:
48
- Exclude:
49
- - 'lib/schema-model.rb'
50
-
51
- # DSL methods following ActiveRecord conventions (has_one, has_many)
52
- Naming/PredicatePrefix:
53
- Exclude:
54
- - 'lib/schema/associations/has_one.rb'
55
- - 'lib/schema/associations/has_many.rb'
56
- - 'lib/schema/associations/schema_creator.rb'
57
-
58
- # Dynamic eval is used for metaprogramming in this DSL gem
59
- Style/DocumentDynamicEvalDefinition:
60
- Enabled: false
61
-
62
- # Short parameter names acceptable in simple setters
63
- Naming/MethodParameterName:
64
- AllowedNames:
65
- - v
66
-
67
- # Boolean parameters acceptable in internal methods
68
- Style/OptionalBooleanParameter:
69
- Enabled: false
data/.ruby-gemset DELETED
@@ -1 +0,0 @@
1
- schema
data/.ruby-version DELETED
@@ -1 +0,0 @@
1
- ruby-4.0.7