web_function 0.5.0 → 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.
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WebFunction
4
+ module Type
5
+ class ArrayOf
6
+ attr_reader :base_type, :of
7
+
8
+ def initialize(of)
9
+ @base_type = "array"
10
+ @of = of
11
+ end
12
+
13
+ def refinement
14
+ nil
15
+ end
16
+
17
+ def inspect
18
+ "#<ArrayOf #{@of.inspect}>"
19
+ end
20
+
21
+ def format(format = :default)
22
+ case format
23
+ when :base
24
+ "array"
25
+ when :compact, :default
26
+ "array<#{@of.format(format)}>"
27
+ else
28
+ raise ArgumentError, "unknown format: #{format.inspect}"
29
+ end
30
+ end
31
+
32
+ def to_s
33
+ format(:default)
34
+ end
35
+
36
+ def ==(other)
37
+ other.is_a?(ArrayOf) && other.of == @of
38
+ end
39
+ alias eql? ==
40
+
41
+ def hash
42
+ [ArrayOf, @of].hash
43
+ end
44
+
45
+ def objects
46
+ @of.objects
47
+ end
48
+
49
+ def without_refinements
50
+ ArrayOf.new(@of.without_refinements)
51
+ end
52
+
53
+ def valid?(value)
54
+ value.is_a?(Array) && value.all? { |element| @of.valid?(element) }
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WebFunction
4
+ module Type
5
+ class Base
6
+ attr_reader :base_type, :refinement
7
+
8
+ def initialize(base_type:, refinement: nil)
9
+ @base_type = base_type
10
+ @refinement = refinement
11
+ end
12
+
13
+ def inspect
14
+ if @refinement
15
+ "#<#{@base_type} #{@refinement}>"
16
+ else
17
+ "#<#{@base_type}>"
18
+ end
19
+ end
20
+
21
+ def format(format = :default)
22
+ case format
23
+ when :compact
24
+ @refinement || @base_type
25
+ when :base
26
+ @base_type
27
+ when :default
28
+ if @refinement
29
+ "#{@base_type}.#{@refinement}"
30
+ else
31
+ @base_type
32
+ end
33
+ else
34
+ raise ArgumentError, "unknown format: #{format.inspect}"
35
+ end
36
+ end
37
+
38
+ def to_s
39
+ format(:default)
40
+ end
41
+
42
+ def ==(other)
43
+ other.is_a?(Base) && other.base_type == @base_type && other.refinement == @refinement
44
+ end
45
+ alias eql? ==
46
+
47
+ def hash
48
+ [Base, @base_type, @refinement].hash
49
+ end
50
+
51
+ def objects
52
+ if @base_type == "object" && @refinement
53
+ [@refinement]
54
+ else
55
+ []
56
+ end
57
+ end
58
+
59
+ def without_refinements
60
+ return self if @refinement.nil?
61
+
62
+ Base.new(base_type: @base_type)
63
+ end
64
+
65
+ def valid?(value)
66
+ case @base_type
67
+ when "string"
68
+ value.is_a?(String) && refinement_valid?(value)
69
+ when "number"
70
+ value.is_a?(Numeric) && !value.is_a?(Complex) && refinement_valid?(value)
71
+ when "object"
72
+ value.is_a?(Hash)
73
+ when "boolean"
74
+ value == true || value == false
75
+ when "null"
76
+ value.nil?
77
+ else
78
+ false
79
+ end
80
+ end
81
+
82
+ private
83
+
84
+ def refinement_valid?(value)
85
+ @refinement.nil? || REFINEMENT_VALIDATORS.fetch(@refinement).call(value)
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WebFunction
4
+ module Type
5
+ class Union
6
+ attr_reader :members
7
+
8
+ def initialize(members)
9
+ @members = members
10
+ end
11
+
12
+ def base_type
13
+ nil
14
+ end
15
+
16
+ def refinement
17
+ nil
18
+ end
19
+
20
+ def inspect
21
+ "#<Union #{@members.map(&:inspect).join(" | ")}>"
22
+ end
23
+
24
+ def format(format = :default)
25
+ @members.map { |member| member.format(format) }.join(" | ")
26
+ end
27
+
28
+ def to_s
29
+ format(:default)
30
+ end
31
+
32
+ def ==(other)
33
+ other.is_a?(Union) && other.members == @members
34
+ end
35
+ alias eql? ==
36
+
37
+ def hash
38
+ [Union, @members].hash
39
+ end
40
+
41
+ def objects
42
+ @members.flat_map(&:objects).uniq
43
+ end
44
+
45
+ def without_refinements
46
+ Type.union(@members.map(&:without_refinements))
47
+ end
48
+
49
+ def valid?(value)
50
+ @members.any? { |member| member.valid?(value) }
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,140 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ipaddr"
4
+ require "uri"
5
+
6
+ require_relative "type/base"
7
+ require_relative "type/array_of"
8
+ require_relative "type/union"
9
+ require_relative "type/any"
10
+
11
+ module WebFunction
12
+ module Type
13
+ ALLOWED_REFINEMENTS = {
14
+ "number" => %w[u32 u64 i32 i64 f32 f64 timestamp].freeze,
15
+ "string" => %w[date time datetime uuid base64 email phone url uri ipv4 ipv6 hostname].freeze,
16
+ }.freeze
17
+
18
+ # Value-level validators for each refinement, keyed by refinement name. Refinement names are unique across base
19
+ # types, so a flat table is enough. Must stay in sync with {ALLOWED_REFINEMENTS}.
20
+ REFINEMENT_VALIDATORS = {
21
+ "u32" => ->(v) { v.is_a?(Integer) && v.between?(0, 0xFFFFFFFF) },
22
+ "u64" => ->(v) { v.is_a?(Integer) && v.between?(0, 0xFFFFFFFFFFFFFFFF) },
23
+ "i32" => ->(v) { v.is_a?(Integer) && v.between?(-0x80000000, 0x7FFFFFFF) },
24
+ "i64" => ->(v) { v.is_a?(Integer) && v.between?(-0x8000000000000000, 0x7FFFFFFFFFFFFFFF) },
25
+ "f32" => ->(v) { v.is_a?(Numeric) && v.to_f.finite? && v.to_f.abs <= 3.4028235e38 },
26
+ "f64" => ->(v) { v.is_a?(Numeric) && v.to_f.finite? },
27
+ "timestamp" => ->(v) { v.is_a?(Integer) && v >= 0 },
28
+ "date" => ->(v) { v.match?(/\A\d{4}-\d{2}-\d{2}\z/) },
29
+ "time" => ->(v) { v.match?(/\A\d{2}:\d{2}:\d{2}(\.\d+)?\z/) },
30
+ "datetime" => ->(v) { v.match?(/\A\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})?\z/) },
31
+ "uuid" => ->(v) { v.match?(/\A\h{8}-\h{4}-\h{4}-\h{4}-\h{12}\z/) },
32
+ "base64" => ->(v) { (v.length % 4).zero? && v.match?(%r{\A[A-Za-z0-9+/]*={0,2}\z}) },
33
+ "email" => ->(v) { v.match?(/\A[^@\s]+@[^@\s]+\.[^@\s]+\z/) },
34
+ "phone" => ->(v) { v.match?(/\A\+[1-9]\d{1,14}\z/) },
35
+ "url" => ->(v) { (uri = URI.parse(v)).is_a?(URI::HTTP) && !uri.host.nil? rescue false },
36
+ "uri" => ->(v) { !URI.parse(v).scheme.nil? rescue false },
37
+ "ipv4" => ->(v) { IPAddr.new(v).ipv4? rescue false },
38
+ "ipv6" => ->(v) { IPAddr.new(v).ipv6? rescue false },
39
+ "hostname" => lambda do |v|
40
+ v.match?(%r{\A(?=.{1,253}\z)([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*\z})
41
+ end,
42
+ }.freeze
43
+
44
+ def self.parse(raw)
45
+ types = [*raw].map { |type| Type.detect(type) }.compact
46
+
47
+ if types.empty?
48
+ return Type.any
49
+ end
50
+
51
+ Type.union(types)
52
+ end
53
+
54
+ def self.detect(raw)
55
+ case raw
56
+ when String
57
+ Type.base(raw)
58
+ when Array
59
+ types = raw.map { |type| Type.detect(type) }.compact
60
+
61
+ if types.empty?
62
+ return Type.array(Type.any)
63
+ end
64
+
65
+ Type.array(Type.union(types))
66
+ else
67
+ return nil
68
+ end
69
+ end
70
+
71
+ def self.base(type)
72
+ base_type, refinement = type.split(".", 2)
73
+
74
+ case base_type
75
+ when "string"
76
+ if refinement && !ALLOWED_REFINEMENTS["string"].include?(refinement)
77
+ return Type.string
78
+ end
79
+
80
+ Type.string(refinement)
81
+ when "number"
82
+ if refinement && !ALLOWED_REFINEMENTS["number"].include?(refinement)
83
+ return Type.number
84
+ end
85
+
86
+ Type.number(refinement)
87
+ when "object"
88
+ Type.object(refinement)
89
+ when "array"
90
+ Type.array
91
+ when "boolean"
92
+ Type.boolean
93
+ when "null"
94
+ Type.null
95
+ when "any"
96
+ Type.any
97
+ else
98
+ nil
99
+ end
100
+ end
101
+
102
+ def self.string(refinement = nil)
103
+ Base.new(base_type: "string", refinement: refinement)
104
+ end
105
+
106
+ def self.number(refinement = nil)
107
+ Base.new(base_type: "number", refinement: refinement)
108
+ end
109
+
110
+ def self.object(refinement = nil)
111
+ Base.new(base_type: "object", refinement: refinement)
112
+ end
113
+
114
+ def self.array(of = any)
115
+ ArrayOf.new(of)
116
+ end
117
+
118
+ def self.boolean
119
+ Base.new(base_type: "boolean")
120
+ end
121
+
122
+ def self.null
123
+ Base.new(base_type: "null")
124
+ end
125
+
126
+ def self.union(types)
127
+ types = types.uniq
128
+
129
+ if types.length > 1
130
+ Union.new(types)
131
+ else
132
+ types.first
133
+ end
134
+ end
135
+
136
+ def self.any
137
+ Any.new
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WebFunction
4
+ # Internal utility methods.
5
+ #
6
+ # @api private
7
+ #
8
+ module Utils
9
+ module_function
10
+
11
+ # Normalizes a collection. Yields each item to the block if a block is given, otherwise returns the item.
12
+ # Any `nil` items are removed.
13
+ #
14
+ # @param collection [Array] The collection to normalize
15
+ #
16
+ # @return [Array] The normalized array
17
+ #
18
+ def normalize_array(collection)
19
+ unless collection.is_a?(Array)
20
+ return []
21
+ end
22
+
23
+ items = collection.map do |item|
24
+ if block_given?
25
+ yield item
26
+ else
27
+ item
28
+ end
29
+ end
30
+
31
+ items.compact
32
+ end
33
+
34
+ # Normalizes an array of strings. Uses #normalize_array under the hood.
35
+ #
36
+ # @param collection [Array] The collection to normalize
37
+ #
38
+ # @return [Array] The normalized array
39
+ #
40
+ def normalize_array_of_strings(collection)
41
+ normalize_array(collection) { |item| item.to_s }
42
+ end
43
+
44
+ def get_body_from_url(url, extra_query_params: {})
45
+ url = add_query_params(url, extra_query_params)
46
+ response = ::Excon.get(url, headers: {
47
+ "User-Agent": "webfunction/#{::WebFunction::VERSION}",
48
+ "Accept-Encoding": "gzip",
49
+ })
50
+
51
+ response.body
52
+ end
53
+
54
+ def add_query_params(url, params = {})
55
+ uri = ::URI.parse(url)
56
+ existing_params = ::URI.decode_www_form(uri.query || "").to_h
57
+ new_params = params.reject { |_, value| value.nil? }.transform_keys(&:to_s)
58
+ merged_params = existing_params.merge(new_params)
59
+
60
+ unless merged_params.empty?
61
+ uri.query = ::URI.encode_www_form(merged_params)
62
+ end
63
+
64
+ uri.to_s
65
+ end
66
+ end
67
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module WebFunction
4
- VERSION = "0.5.0"
4
+ VERSION = "0.8.0"
5
5
  end
data/lib/web_function.rb CHANGED
@@ -5,35 +5,47 @@ require "json"
5
5
  require "uri"
6
6
 
7
7
  require_relative "web_function/version"
8
+ require_relative "web_function/request"
8
9
  require_relative "web_function/client"
10
+ require_relative "web_function/flaggable"
9
11
  require_relative "web_function/package"
10
12
  require_relative "web_function/endpoint"
11
13
  require_relative "web_function/argument"
12
14
  require_relative "web_function/attribute"
15
+ require_relative "web_function/object_schema"
13
16
  require_relative "web_function/documented_error"
14
17
  require_relative "web_function/pipeline"
15
18
  require_relative "web_function/promise"
19
+ require_relative "web_function/type"
20
+ require_relative "web_function/utils"
16
21
 
17
22
  module WebFunction
23
+ # A base error class for WebFunction. All errors inherit from this class.
24
+ #
25
+ # @api private
26
+ #
18
27
  class Error < StandardError
19
28
  attr_reader :code, :details
20
29
 
21
- def initialize(message = nil, code: self.class.name, details: nil)
30
+ def initialize(message = nil, code: self.class.error_code, details: nil)
22
31
  super(message)
23
32
  @code = code
24
33
  @details = details
25
34
  end
26
- end
27
-
28
- class UnresolvedPromiseError < Error
29
- end
30
35
 
31
- class UnexpectedStatusCodeError < Error
32
- end
33
-
34
- class JsonParseError < Error
36
+ # Returns the error code for the error. Used as default error code if no code is provided.
37
+ #
38
+ # @return [String] The error code
39
+ #
40
+ def self.error_code
41
+ name.sub(/^WebFunction::/, "WFN_")
42
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
43
+ .upcase
44
+ end
35
45
  end
36
46
 
37
- class BadRequestError < Error
38
- end
47
+ UnresolvedPromiseError = Class.new(Error)
48
+ UnexpectedStatusCodeError = Class.new(Error)
49
+ JsonParseError = Class.new(Error)
50
+ BadRequestError = Class.new(Error)
39
51
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: web_function
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Robin Clart
@@ -9,6 +9,34 @@ bindir: exe
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: irb
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: logger
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
12
40
  - !ruby/object:Gem::Dependency
13
41
  name: excon
14
42
  requirement: !ruby/object:Gem::Requirement
@@ -60,9 +88,18 @@ files:
60
88
  - lib/web_function/client.rb
61
89
  - lib/web_function/documented_error.rb
62
90
  - lib/web_function/endpoint.rb
91
+ - lib/web_function/flaggable.rb
92
+ - lib/web_function/object_schema.rb
63
93
  - lib/web_function/package.rb
64
94
  - lib/web_function/pipeline.rb
65
95
  - lib/web_function/promise.rb
96
+ - lib/web_function/request.rb
97
+ - lib/web_function/type.rb
98
+ - lib/web_function/type/any.rb
99
+ - lib/web_function/type/array_of.rb
100
+ - lib/web_function/type/base.rb
101
+ - lib/web_function/type/union.rb
102
+ - lib/web_function/utils.rb
66
103
  - lib/web_function/version.rb
67
104
  - sig/web_function.rbs
68
105
  homepage: https://github.com/robinclart/web_function