jet-type 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: fa0ae83d7ac14ae3dfa9a513e9767dab4ec2fbdca0c28a6383a7d3ea81414347
4
+ data.tar.gz: 4620b255cab2acb2563fa2ce71d9c75fb7fa80e131bc6a7127cba9613551e3fe
5
+ SHA512:
6
+ metadata.gz: 86d97024d6cb5535cacc7714639b718dd96716fb00341da8474aeaf94b150102bcc7636d027dabcc425263e8fdefebfc2fc37543d75671338c4a226b710bbe7d
7
+ data.tar.gz: 8a4e0eecea09ce47a7c2204da5bf4ebf6e0d50f47484d80c621ad8229ed18beacf5629bdff6d6ee524a0412bb9bc5530dbf7f4517578857953d0a77f3e077113
data/LICENSE.txt ADDED
@@ -0,0 +1,18 @@
1
+ Copyright (c) 2019 Joshua Hansen
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to
5
+ deal in the Software without restriction, including without limitation the
6
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
7
+ sell copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
16
+ THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
17
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
18
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,3 @@
1
+ # Jet Types: Safe Type Coercion for the Jet Toolkit
2
+
3
+ TODO: Write.
data/lib/jet-type.rb ADDED
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "jet/type"
data/lib/jet/type.rb ADDED
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "jet/core"
4
+ require "jet/type/coercion"
5
+ require "jet/type/version"
6
+
7
+ module Jet
8
+ class Type
9
+ def self.with(type, *types, name: nil, &blk)
10
+ Jet.type_check!("`type`", type, Type)
11
+ new(
12
+ name || type.name,
13
+ *[type.types, types].flatten.uniq,
14
+ coercions: type.coercions,
15
+ filter: type.filter,
16
+ &blk
17
+ )
18
+ end
19
+
20
+ attr_reader :coercions, :name, :types
21
+
22
+ def initialize(name, *types, coercions: [], filter: nil, &blk)
23
+ @name = name.to_sym
24
+ @coercions = coercions.dup
25
+ @filter = filter
26
+ @types = Jet.type_check_each!("`types`", types, Class, Module)
27
+ instance_eval(&blk) if block_given?
28
+ @coercions.freeze
29
+ end
30
+
31
+ def call(input)
32
+ return process_output(input) if type_match?(input)
33
+
34
+ @coercions.each do |coercion|
35
+ result = coercion.(input)
36
+ return process_output(result.output) if result.success?
37
+ return result if result.failure? && result != :no_coercion_match
38
+ end
39
+
40
+ failure(input: input)
41
+ end
42
+
43
+ def filter(callable = nil, &blk)
44
+ return @filter unless callable || block_given?
45
+ @filter = Core.block_or_callable!(callable, &blk)
46
+ end
47
+
48
+ def inspect
49
+ "#<#{self.class.name}:#{name}>"
50
+ end
51
+
52
+ def maybe
53
+ @maybe ||= maybe? ? self : self.class.with(self, NilClass)
54
+ end
55
+
56
+ def maybe?
57
+ types.include?(NilClass)
58
+ end
59
+
60
+ def to_sym
61
+ name
62
+ end
63
+
64
+ def type_match?(obj)
65
+ types.any? { |t| obj.is_a?(t) }
66
+ end
67
+
68
+ private
69
+
70
+ def coerce(at = :after, &blk)
71
+ coercion = Coercion.new(&blk)
72
+ if at == :before
73
+ @coercions = [coercion] + coercions
74
+ elsif at == :after
75
+ @coercions += [coercion]
76
+ else
77
+ raise ArgumentError, "`at` must equal :before or :after"
78
+ end
79
+ end
80
+
81
+ def failure(error = :type_coercion_failure, **context)
82
+ Result.failure([error, name], context.merge(types: types))
83
+ end
84
+
85
+ def process_output(output)
86
+ output &&= @filter ? @filter.(output) : output
87
+ return failure(output: output) unless type_match?(output)
88
+ Result.success(output)
89
+ end
90
+ end
91
+ end
92
+
93
+ require "jet/core/instance_registry"
94
+ require "jet/type/strict"
95
+ require "jet/type/http"
96
+ require "jet/type/json"
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jet
4
+ class Type
5
+ class Coercion
6
+ def initialize(&blk)
7
+ raise ArgumentError, "no block given" unless block_given?
8
+ @checks = []
9
+ @matchers = []
10
+ instance_eval(&blk)
11
+ raise ArgumentError, "no `match` blocks given" if @matchers.empty?
12
+ @checks = @checks.freeze
13
+ @matchers.freeze
14
+ @transformer ||= proc { |input| input }
15
+ end
16
+
17
+ def call(input)
18
+ return Result.failure(:no_coercion_match, input: input) unless match?(input)
19
+
20
+ catch(:transformation_failure) do
21
+ check_output(input, instance_exec(input, &@transformer))
22
+ end
23
+ end
24
+
25
+ def check_output(input, output)
26
+ @checks.each do |(check, error)|
27
+ return coercion_check_failure(output, input, error) unless check.(output, input)
28
+ end
29
+ Result.success(output, input: input)
30
+ end
31
+
32
+ def coercion_check_failure(output, input, error)
33
+ Result.failure(:coercion_check_failure, errors: error, input: input, output: output)
34
+ end
35
+
36
+ def match?(output)
37
+ @matchers.any? { |blk| blk.(output) }
38
+ end
39
+
40
+ def transformation_failure(input, *errors)
41
+ Result.failure(:transformation_failure, errors: errors, input: input)
42
+ end
43
+
44
+ def transformation_failure!(*args)
45
+ throw :transformation_failure, transformation_failure(*args)
46
+ end
47
+
48
+ private
49
+
50
+ def check(error = nil, &blk)
51
+ @checks += [[blk, error]]
52
+ end
53
+
54
+ def match(&blk)
55
+ @matchers += [blk]
56
+ end
57
+
58
+ def transform(&blk)
59
+ @transformer = blk
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jet
4
+ class Type
5
+ module HTTP
6
+ extend Core::InstanceRegistry
7
+ type Type
8
+
9
+ module Matcher
10
+ Numeric = proc do |input|
11
+ input.is_a?(::String) && input.match?(/\A-?\d+(\.\d+)?\Z/)
12
+ end
13
+ end
14
+
15
+ DATE_PATTERN = /\d{4}(-\d{2}){2}/.freeze
16
+ TIME_PATTERN = /(\d{2}:){2}\d{2}(\.0{1-3})?(Z?|[-+]\d{2}:\d{2})?/.freeze
17
+ DATETIME_PATTERN = /#{DATE_PATTERN}[ T]#{TIME_PATTERN}/.freeze
18
+
19
+ FALSE_VALUES = %w[0 f false F FALSE n no N NO].freeze
20
+ TRUE_VALUES = %w[1 t true T TRUE y yes Y YES].freeze
21
+
22
+ Boolean = Type.with(Strict::Boolean) do
23
+ coerce do
24
+ match { |input| TRUE_VALUES.any? { |v| input == v } }
25
+ transform { true }
26
+ end
27
+
28
+ coerce do
29
+ match { |input| FALSE_VALUES.any? { |v| input == v } }
30
+ transform { false }
31
+ end
32
+ end
33
+
34
+ Date = Type.with(Strict::Date) do
35
+ coerce do
36
+ match { |input| input.is_a?(String) && input.match?(/\A#{DATE_PATTERN}\Z/) }
37
+
38
+ transform do |input|
39
+ args = input.split("-").map(&:to_i)
40
+ transformation_failure!(input, :invalid_date) unless ::Date.valid_date?(*args)
41
+ ::Date.new(*args)
42
+ end
43
+ end
44
+ end
45
+
46
+ Decimal = Type.with(Strict::Decimal) do
47
+ coerce do
48
+ match(&Matcher::Numeric)
49
+ transform { |input| BigDecimal(input) }
50
+ end
51
+ end
52
+
53
+ Float = Type.with(Strict::Float) do
54
+ coerce do
55
+ match(&Matcher::Numeric)
56
+ transform(&:to_f)
57
+ end
58
+ end
59
+
60
+ Integer = Type.with(Strict::Integer) do
61
+ coerce do
62
+ match { |input| input.is_a?(::String) && input.match?(/\A-?\d+(\.0+)?\Z/) }
63
+ transform(&:to_i)
64
+ end
65
+ end
66
+
67
+ Time = Type.with(Strict::Time) do
68
+ coerce do
69
+ match { |input| input.is_a?(String) && input.match?(/\A#{DATETIME_PATTERN}\Z/) }
70
+
71
+ transform do |input|
72
+ date, time = input.split(/[ T]/)
73
+ date_args = date.split("-").map(&:to_i)
74
+ transformation_failure!(input, :invalid_date) unless ::Date.valid_date?(*date_args)
75
+
76
+ h = time[0..1].to_i
77
+ transformation_failure!(input, :invalid_hours) if h > 24
78
+ m = time[3..4].to_i
79
+ transformation_failure!(input, :invalid_minutes) if m > 59
80
+ s = time[6..7].to_i
81
+ transformation_failure!(input, :invalid_seconds) if s > 59
82
+
83
+ offset =
84
+ if (idx = time.index(/[-+]/))
85
+ time[idx..-1].tap do |o|
86
+ transformation_failure!(input, :invalid_utc_offest) if
87
+ !%w[00 15 30 45].include?(o[4..5]) || o[1..2].to_i > 12
88
+ end
89
+ else
90
+ "+00:00"
91
+ end
92
+
93
+ ::Time.new(*date_args, h, m, s, offset)
94
+ end
95
+ end
96
+ end
97
+
98
+ register(
99
+ **Strict.to_h,
100
+ boolean: Boolean,
101
+ date: Date,
102
+ decimal: Decimal,
103
+ float: Float,
104
+ integer: Integer,
105
+ time: Time
106
+ ).freeze
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jet
4
+ class Type
5
+ module JSON
6
+ extend Core::InstanceRegistry
7
+ type Type
8
+
9
+ module Matcher
10
+ SUPPORTED_NUMERICS = [::BigDecimal, ::Float, ::Integer].freeze
11
+
12
+ Numeric = proc do |input|
13
+ SUPPORTED_NUMERICS.any? { |type| input.is_a?(type) }
14
+ end
15
+ end
16
+
17
+ Decimal = Type.with(HTTP::Decimal) do
18
+ coerce(:before) do
19
+ match(&Matcher::Numeric)
20
+ transform do |input|
21
+ case input
22
+ when ::Float
23
+ BigDecimal(input, ::Float::DIG)
24
+ else
25
+ BigDecimal(input)
26
+ end
27
+ end
28
+ end
29
+ end
30
+
31
+ Float = Type.with(Strict::Float) do
32
+ coerce do
33
+ match(&Matcher::Numeric)
34
+ transform(&:to_f)
35
+ end
36
+ end
37
+
38
+ Integer = Type.with(Strict::Integer) do
39
+ coerce do
40
+ match(&Matcher::Numeric)
41
+ transform(&:to_i)
42
+ check(:number_too_precise) { |output, input| input == output }
43
+ end
44
+ end
45
+
46
+ register(
47
+ **HTTP.to_h,
48
+ decimal: Decimal,
49
+ float: Float,
50
+ integer: Integer
51
+ ).freeze
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bigdecimal"
4
+ require "date"
5
+ require "time"
6
+
7
+ module Jet
8
+ class Type
9
+ module Strict
10
+ extend Core::InstanceRegistry
11
+ type Type
12
+
13
+ [
14
+ Array = Type.new(:array, ::Array),
15
+ Boolean = Type.new(:boolean, TrueClass, FalseClass),
16
+ Date = Type.new(:date, ::Date),
17
+ Decimal = Type.new(:decimal, BigDecimal),
18
+ Float = Type.new(:float, ::Float),
19
+ Hash = Type.new(:hash, ::Hash),
20
+ Integer = Type.new(:integer, ::Integer),
21
+ String = Type.new(:string, ::String),
22
+ Time = Type.new(:time, ::Time)
23
+ ].map { |t| [t.name, t] }.to_h.tap { |types| register(types).freeze }
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jet
4
+ class Type
5
+ MAJOR = 0
6
+ MINOR = 1
7
+ TINY = 0
8
+ VERSION = [MAJOR, MINOR, TINY].join(".").freeze
9
+
10
+ def self.version
11
+ VERSION
12
+ end
13
+ end
14
+ end
metadata ADDED
@@ -0,0 +1,138 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: jet-type
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Joshua
8
+ - Hansen
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2019-11-08 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: jet-core
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - "~>"
19
+ - !ruby/object:Gem::Version
20
+ version: 0.1.0
21
+ type: :runtime
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - "~>"
26
+ - !ruby/object:Gem::Version
27
+ version: 0.1.0
28
+ - !ruby/object:Gem::Dependency
29
+ name: bundler
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - "~>"
33
+ - !ruby/object:Gem::Version
34
+ version: '2.0'
35
+ type: :development
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - "~>"
40
+ - !ruby/object:Gem::Version
41
+ version: '2.0'
42
+ - !ruby/object:Gem::Dependency
43
+ name: m
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - "~>"
47
+ - !ruby/object:Gem::Version
48
+ version: '1.5'
49
+ type: :development
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - "~>"
54
+ - !ruby/object:Gem::Version
55
+ version: '1.5'
56
+ - !ruby/object:Gem::Dependency
57
+ name: minitest
58
+ requirement: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - "~>"
61
+ - !ruby/object:Gem::Version
62
+ version: '5.0'
63
+ type: :development
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - "~>"
68
+ - !ruby/object:Gem::Version
69
+ version: '5.0'
70
+ - !ruby/object:Gem::Dependency
71
+ name: rake
72
+ requirement: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - "~>"
75
+ - !ruby/object:Gem::Version
76
+ version: '10.0'
77
+ type: :development
78
+ prerelease: false
79
+ version_requirements: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - "~>"
82
+ - !ruby/object:Gem::Version
83
+ version: '10.0'
84
+ - !ruby/object:Gem::Dependency
85
+ name: rubocop
86
+ requirement: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - "~>"
89
+ - !ruby/object:Gem::Version
90
+ version: '0.56'
91
+ type: :development
92
+ prerelease: false
93
+ version_requirements: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - "~>"
96
+ - !ruby/object:Gem::Version
97
+ version: '0.56'
98
+ description: Safe type coercion for the Jet Toolkit.
99
+ email:
100
+ - joshua@epicbanality.com
101
+ executables: []
102
+ extensions: []
103
+ extra_rdoc_files: []
104
+ files:
105
+ - LICENSE.txt
106
+ - README.md
107
+ - lib/jet-type.rb
108
+ - lib/jet/type.rb
109
+ - lib/jet/type/coercion.rb
110
+ - lib/jet/type/http.rb
111
+ - lib/jet/type/json.rb
112
+ - lib/jet/type/strict.rb
113
+ - lib/jet/type/version.rb
114
+ homepage: https://github.com/binarypaladin/jet-type
115
+ licenses:
116
+ - MIT
117
+ metadata: {}
118
+ post_install_message:
119
+ rdoc_options: []
120
+ require_paths:
121
+ - lib
122
+ required_ruby_version: !ruby/object:Gem::Requirement
123
+ requirements:
124
+ - - ">="
125
+ - !ruby/object:Gem::Version
126
+ version: 2.5.0
127
+ required_rubygems_version: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ requirements: []
133
+ rubyforge_project:
134
+ rubygems_version: 2.7.6.2
135
+ signing_key:
136
+ specification_version: 4
137
+ summary: Safe type coercion for the Jet Toolkit.
138
+ test_files: []