web_function 0.6.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,135 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WebFunction
4
+ # Represents a named object definition as described in a Web Function package.
5
+ #
6
+ # Objects are declared in a package under the `"objects"` key and can be referenced as a refined object type
7
+ # (`object.<name>`) anywhere a type is expected. An `object.` reference appears in one of two contexts, which
8
+ # determines which set of properties applies:
9
+ #
10
+ # - Argument context — the object is referenced as an argument's `type`. Its {#arguments} describe its properties.
11
+ # - Attribute context — the object is referenced as an endpoint's `returns` or as an attribute's `type`. Its
12
+ # {#attributes} describe its properties.
13
+ #
14
+ # Because an object MAY be referenced in both contexts within the same package, it MAY define both `arguments` and
15
+ # `attributes`; each set is used only in its matching context.
16
+ #
17
+ # It is named `ObjectSchema` rather than `Object` to avoid clashing with Ruby's built-in `::Object`.
18
+ #
19
+ # See the [object definition documentation][0] on the Web Function website for more details.
20
+ #
21
+ # [0]: https://webfunction.org/package#object-definition
22
+ #
23
+ class ObjectSchema
24
+ # The contexts in which an object may be referenced. Each maps to the member set that applies in that context.
25
+ #
26
+ # @return [Array<Symbol>]
27
+ #
28
+ CONTEXTS = %i[arguments attributes].freeze
29
+
30
+ def initialize(name:, arguments: [], attributes: [])
31
+ @name = name
32
+ @arguments = arguments.to_h { |a| [a.name, a] }
33
+ @attributes = attributes.to_h { |a| [a.name, a] }
34
+ end
35
+
36
+ class << self
37
+ # Creates a new ObjectSchema from a hash. Typically coming from a {Package}.
38
+ #
39
+ # @param object [Hash] The object hash
40
+ #
41
+ # @return [ObjectSchema, nil] A new ObjectSchema instance, or `nil` if the hash is invalid.
42
+ #
43
+ def from_hash(object)
44
+ unless object.is_a?(Hash)
45
+ return
46
+ end
47
+
48
+ unless object["name"]
49
+ return
50
+ end
51
+
52
+ new(
53
+ name: object["name"],
54
+ arguments: Argument.from_array(object["arguments"]),
55
+ attributes: Attribute.from_array(object["attributes"]),
56
+ )
57
+ end
58
+
59
+ # Creates a new ObjectSchema from an array of hashes. Typically coming from a {Package}. Uses
60
+ # {ObjectSchema#from_hash} under the hood.
61
+ #
62
+ # @param objects [Array<Hash>] The object array of hashes
63
+ #
64
+ # @return [Array<ObjectSchema>] A new array of ObjectSchema instances
65
+ #
66
+ def from_array(objects)
67
+ Utils.normalize_array objects do |object|
68
+ from_hash(object)
69
+ end
70
+ end
71
+ end
72
+
73
+ # The name of the object. It is referenced as a refined object type (`object.<name>`) and is unique within a
74
+ # package.
75
+ #
76
+ # @return [String]
77
+ #
78
+ attr_reader :name
79
+
80
+ # The object's properties when it is referenced in an argument context.
81
+ #
82
+ # @return [Array<Argument>]
83
+ #
84
+ def arguments
85
+ @arguments.values
86
+ end
87
+
88
+ # Looks up a single argument member by name.
89
+ #
90
+ # @param name [String, Symbol] The name of the argument to look up.
91
+ #
92
+ # @return [Argument, nil] The matching argument, or `nil` if none is found.
93
+ #
94
+ def argument(name)
95
+ @arguments[name.to_s]
96
+ end
97
+
98
+ # The object's properties when it is referenced in an attribute context.
99
+ #
100
+ # @return [Array<Attribute>]
101
+ #
102
+ def attributes
103
+ @attributes.values
104
+ end
105
+
106
+ # Looks up a single attribute member by name.
107
+ #
108
+ # @param name [String, Symbol] The name of the attribute to look up.
109
+ #
110
+ # @return [Attribute, nil] The matching attribute, or `nil` if none is found.
111
+ #
112
+ def attribute(name)
113
+ @attributes[name.to_s]
114
+ end
115
+
116
+ # The object's properties for the given context.
117
+ #
118
+ # @param context [Symbol] The context to resolve properties for. One of {CONTEXTS}.
119
+ #
120
+ # @raise [ArgumentError] If the context is not one of {CONTEXTS}.
121
+ #
122
+ # @return [Array<Argument>, Array<Attribute>] The properties that apply in the given context.
123
+ #
124
+ def properties(context)
125
+ case context
126
+ when :arguments
127
+ arguments
128
+ when :attributes
129
+ attributes
130
+ else
131
+ raise ArgumentError, "context must be one of #{CONTEXTS.inspect}, got #{context.inspect}"
132
+ end
133
+ end
134
+ end
135
+ end
@@ -16,7 +16,7 @@ module WebFunction
16
16
  include Flaggable
17
17
 
18
18
  def initialize(base_url:, pipeline_url: nil, name: nil, version: nil, docs: nil, flags: [], versions: [],
19
- endpoints: [], errors: [])
19
+ endpoints: [], errors: [], objects: [])
20
20
  @base_url = base_url
21
21
  @pipeline_url = pipeline_url
22
22
  @name = name
@@ -26,6 +26,7 @@ module WebFunction
26
26
  @versions = versions
27
27
  @endpoints = endpoints.to_h { |e| [e.name, e] }
28
28
  @errors = errors.to_h { |e| [e.code, e] }
29
+ @objects = objects.to_h { |o| [o.name, o] }
29
30
  end
30
31
 
31
32
  class << self
@@ -46,6 +47,7 @@ module WebFunction
46
47
  versions: Utils.normalize_array_of_strings(package["versions"]),
47
48
  endpoints: Endpoint.from_array(package["endpoints"]),
48
49
  errors: DocumentedError.from_array(package["errors"]),
50
+ objects: ObjectSchema.from_array(package["objects"]),
49
51
  )
50
52
  end
51
53
  end
@@ -144,6 +146,44 @@ module WebFunction
144
146
  @errors[code.to_s]
145
147
  end
146
148
 
149
+ # The named object definitions declared by this package. Each can be referenced as a refined object type
150
+ # (`object.<name>`) anywhere a type is expected.
151
+ #
152
+ # @return [Array<ObjectSchema>]
153
+ #
154
+ def objects
155
+ @objects.values
156
+ end
157
+
158
+ # Looks up a single object definition by name, resolved for the given context.
159
+ #
160
+ # An `object.` reference appears in either an argument or attribute context, which determines which set of members
161
+ # applies. The `context` selects the member set that is relevant; an object that does not define members for the
162
+ # requested context is treated as absent and `nil` is returned.
163
+ #
164
+ # @param name [String, Symbol] The name of the object to look up.
165
+ # @param context [Symbol] The context the object is referenced in. One of {ObjectSchema::CONTEXTS}
166
+ # (`:arguments` or `:attributes`).
167
+ #
168
+ # @raise [ArgumentError] If the context is not one of {ObjectSchema::CONTEXTS}.
169
+ #
170
+ # @return [ObjectSchema, nil] The matching object, or `nil` if none is found or it defines no members for the
171
+ # given context.
172
+ #
173
+ def object(name, context:)
174
+ object = @objects[name.to_s]
175
+
176
+ unless object
177
+ return
178
+ end
179
+
180
+ if object.properties(context).empty?
181
+ return
182
+ end
183
+
184
+ object
185
+ end
186
+
147
187
  # Whether the package is versioned, i.e. whether it declares the `versioned` flag. A versioned package is selected
148
188
  # using the `Api-Version` header.
149
189
  #
@@ -94,6 +94,7 @@ module WebFunction
94
94
  "Content-Type": "application/json",
95
95
  "Accept": "application/json",
96
96
  "User-Agent": "webfunction/#{WebFunction::VERSION}",
97
+ "Accept-Encoding": "gzip",
97
98
  }
98
99
 
99
100
  if @bearer_auth
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WebFunction
4
+ module Type
5
+ class Any
6
+ def base_type
7
+ "any"
8
+ end
9
+
10
+ def refinement
11
+ nil
12
+ end
13
+
14
+ def inspect
15
+ "#<any>"
16
+ end
17
+
18
+ def format(_format = :default)
19
+ "any"
20
+ end
21
+
22
+ def to_s
23
+ format(:default)
24
+ end
25
+
26
+ def ==(other)
27
+ other.is_a?(Any)
28
+ end
29
+ alias eql? ==
30
+
31
+ def hash
32
+ Any.hash
33
+ end
34
+
35
+ def objects
36
+ []
37
+ end
38
+
39
+ def without_refinements
40
+ self
41
+ end
42
+
43
+ def valid?(_value)
44
+ true
45
+ end
46
+ end
47
+ end
48
+ end
@@ -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
@@ -40,5 +40,28 @@ module WebFunction
40
40
  def normalize_array_of_strings(collection)
41
41
  normalize_array(collection) { |item| item.to_s }
42
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
43
66
  end
44
67
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module WebFunction
4
- VERSION = "0.6.0"
4
+ VERSION = "0.8.0"
5
5
  end
data/lib/web_function.rb CHANGED
@@ -12,9 +12,11 @@ require_relative "web_function/package"
12
12
  require_relative "web_function/endpoint"
13
13
  require_relative "web_function/argument"
14
14
  require_relative "web_function/attribute"
15
+ require_relative "web_function/object_schema"
15
16
  require_relative "web_function/documented_error"
16
17
  require_relative "web_function/pipeline"
17
18
  require_relative "web_function/promise"
19
+ require_relative "web_function/type"
18
20
  require_relative "web_function/utils"
19
21
 
20
22
  module WebFunction