comatype 0.0.2
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.
- checksums.yaml +7 -0
- data/README.md +7 -0
- data/lib/comatype/c.rb +120 -0
- data/lib/comatype/cache.rb +119 -0
- data/lib/comatype/check.rb +46 -0
- data/lib/comatype/codec.rb +266 -0
- data/lib/comatype/cookie.rb +109 -0
- data/lib/comatype/endpoint.rb +151 -0
- data/lib/comatype/errors.rb +37 -0
- data/lib/comatype/model.rb +164 -0
- data/lib/comatype/raw.rb +97 -0
- data/lib/comatype/response.rb +71 -0
- data/lib/comatype/s.rb +234 -0
- data/lib/comatype/wire.rb +28 -0
- data/lib/comatype.rb +26 -0
- metadata +60 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Comatype
|
|
4
|
+
# The request a server handler receives.
|
|
5
|
+
Request = Struct.new(:params, :search, :headers, :body, :ctx, :raw, keyword_init: true)
|
|
6
|
+
|
|
7
|
+
# Endpoint descriptor (held by generated code). Request / response conversion is done here according to the description.
|
|
8
|
+
class Endpoint
|
|
9
|
+
attr_reader :method, :path, :module_name, :base_url, :wraps, :cookie, :cookie_def, :cache, :cache_headers, :params, :search, :headers, :body, :responses
|
|
10
|
+
|
|
11
|
+
# params / search / headers: Model subclasses (nil if absent), body: type description (nil if absent)
|
|
12
|
+
# responses: array of Response subclasses
|
|
13
|
+
def initialize(method:, path:, module_name:, base_url: nil, wraps: [], cookie: false, cookie_def: nil, cache: nil, cache_headers: [],
|
|
14
|
+
params: nil, search: nil, headers: nil, body: nil, responses: [])
|
|
15
|
+
@method = method
|
|
16
|
+
@path = path
|
|
17
|
+
@module_name = module_name
|
|
18
|
+
@base_url = base_url
|
|
19
|
+
@wraps = wraps.map(&:to_s)
|
|
20
|
+
@cookie = cookie
|
|
21
|
+
@cookie_def = cookie_def
|
|
22
|
+
@cache = cache # CacheSpec (GET / QUERY only; nil means no caching)
|
|
23
|
+
@cache_headers = cache_headers # names of declared request headers included in the client cache key
|
|
24
|
+
@params = params
|
|
25
|
+
@search = search
|
|
26
|
+
@headers = headers
|
|
27
|
+
@body = body
|
|
28
|
+
@responses = responses.to_h { |r| [r.status, r] }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# ---- client: arguments (params: search: headers: body:) -> RawRequest ----
|
|
32
|
+
|
|
33
|
+
def build_request(args)
|
|
34
|
+
unknown = args.keys - %i[params search headers body]
|
|
35
|
+
raise ArgumentError, "#{module_name}: 知らない引数 #{unknown.join(', ')}" unless unknown.empty?
|
|
36
|
+
|
|
37
|
+
raw = RawRequest.new(method, path)
|
|
38
|
+
if @params
|
|
39
|
+
p = @params.coerce(args[:params]) || @params.new
|
|
40
|
+
p.validate
|
|
41
|
+
segs = raw.path.split("/", -1).map do |s|
|
|
42
|
+
next s unless s.start_with?(":")
|
|
43
|
+
|
|
44
|
+
name = s[1..]
|
|
45
|
+
f = @params.fields.find { |x| x.json == name }
|
|
46
|
+
v = f && Codec.to_wire_list(f.type, p.public_send(f.attr)).first
|
|
47
|
+
raise ValidationError.new(name, "必須です") if v.nil? || v.empty?
|
|
48
|
+
|
|
49
|
+
raw.params[name] = v
|
|
50
|
+
Cookies.encode(v)
|
|
51
|
+
end
|
|
52
|
+
raw.path = segs.join("/")
|
|
53
|
+
end
|
|
54
|
+
if @search
|
|
55
|
+
s = @search.coerce(args[:search]) || @search.new
|
|
56
|
+
s.validate
|
|
57
|
+
@search.fields.each { |f| Codec.to_wire_list(f.type, s.public_send(f.attr)).each { |v| raw.query << [f.json, v] } }
|
|
58
|
+
end
|
|
59
|
+
if @headers
|
|
60
|
+
h = @headers.coerce(args[:headers]) || @headers.new
|
|
61
|
+
h.validate
|
|
62
|
+
write_headers(raw.headers, @headers, h)
|
|
63
|
+
end
|
|
64
|
+
if @body
|
|
65
|
+
raise ValidationError.new("body", "必須です") if args[:body].nil?
|
|
66
|
+
|
|
67
|
+
b = args[:body]
|
|
68
|
+
b = b.is_a?(Model) ? b : coerce_body(@body, b)
|
|
69
|
+
Codec.validate(@body, b, "")
|
|
70
|
+
raw.body = JSON.generate(Codec.encode(@body, b)).b
|
|
71
|
+
raw.headers["Content-Type"] = "application/json"
|
|
72
|
+
end
|
|
73
|
+
raw
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# ---- client: RawResponse -> Response ----
|
|
77
|
+
|
|
78
|
+
def parse_response(raw)
|
|
79
|
+
cls = @responses[raw.status] or raise UnexpectedStatusError.new(raw.status, raw.body)
|
|
80
|
+
body = nil
|
|
81
|
+
if cls.body_type
|
|
82
|
+
raise ValidationError.new("body", "必須です") if raw.body.empty?
|
|
83
|
+
|
|
84
|
+
body = Codec.decode(cls.body_type, Wire.parse_json(raw.body, "body"), "body")
|
|
85
|
+
end
|
|
86
|
+
headers = nil
|
|
87
|
+
if cls.headers_class
|
|
88
|
+
headers = cls.headers_class.from_wire(->(n) { raw.headers.get_all(n) }, split: true)
|
|
89
|
+
headers.validate
|
|
90
|
+
end
|
|
91
|
+
cls.new(body, headers: headers)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# ---- server: RawRequest -> Request ----
|
|
95
|
+
|
|
96
|
+
def parse_request(raw)
|
|
97
|
+
req = Request.new(ctx: raw.ctx, raw: raw)
|
|
98
|
+
req.params = @params&.from_wire(->(n) { raw.params.key?(n) ? [raw.params[n]] : [] })&.tap(&:validate)
|
|
99
|
+
req.search = @search&.from_wire(->(n) { raw.query_all(n) })&.tap(&:validate)
|
|
100
|
+
req.headers = @headers&.from_wire(->(n) { raw.headers.get_all(n) }, split: true)&.tap(&:validate)
|
|
101
|
+
if @body
|
|
102
|
+
raise ValidationError.new("body", "必須です") if raw.body.empty?
|
|
103
|
+
|
|
104
|
+
req.body = Codec.decode(@body, Wire.parse_json(raw.body, "body"), "body")
|
|
105
|
+
end
|
|
106
|
+
req
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# ---- server: Response -> RawResponse ----
|
|
110
|
+
|
|
111
|
+
def build_response(res)
|
|
112
|
+
cls = @responses[res.status]
|
|
113
|
+
raise ConfigError, "s#{res.status} は #{module_name} に宣言されていません" unless cls && res.is_a?(cls)
|
|
114
|
+
|
|
115
|
+
out = RawResponse.new(res.status)
|
|
116
|
+
if cls.body_type
|
|
117
|
+
Codec.validate(cls.body_type, res.body, "")
|
|
118
|
+
out.body = JSON.generate(Codec.encode(cls.body_type, res.body)).b
|
|
119
|
+
out.headers["Content-Type"] = "application/json"
|
|
120
|
+
end
|
|
121
|
+
if cls.headers_class
|
|
122
|
+
res.headers.validate
|
|
123
|
+
write_headers(out.headers, cls.headers_class, res.headers)
|
|
124
|
+
end
|
|
125
|
+
out.set_cookies << Cookies.serialize(@cookie_def, res.cookie) if res.cookie
|
|
126
|
+
out
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
private
|
|
130
|
+
|
|
131
|
+
def write_headers(headers, klass, model)
|
|
132
|
+
klass.fields.each do |f|
|
|
133
|
+
vs = Codec.to_wire_list(f.type, model.public_send(f.attr))
|
|
134
|
+
headers[f.json] = vs.join(", ") unless vs.empty?
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Hash / array values other than the body go straight through the type description (Model references are built from hashes)
|
|
139
|
+
def coerce_body(t, v)
|
|
140
|
+
kind = Codec.kind_of(t)
|
|
141
|
+
case kind
|
|
142
|
+
when :ref
|
|
143
|
+
k = Codec.resolve(t[1])
|
|
144
|
+
k.respond_to?(:coerce) ? k.coerce(v) : v
|
|
145
|
+
when :array then v.map { |x| coerce_body(t[1], x) }
|
|
146
|
+
when :nullable then v.nil? ? nil : coerce_body(t[1], v)
|
|
147
|
+
else v
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Comatype
|
|
4
|
+
class Error < StandardError; end
|
|
5
|
+
|
|
6
|
+
# An input or output does not match the .comat type.
|
|
7
|
+
class ValidationError < Error
|
|
8
|
+
attr_reader :path, :reason
|
|
9
|
+
|
|
10
|
+
def initialize(path, reason)
|
|
11
|
+
@path = path
|
|
12
|
+
@reason = reason
|
|
13
|
+
super(path.empty? ? reason : "#{path}: #{reason}")
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.join(path, name)
|
|
17
|
+
path.empty? ? name.to_s : "#{path}.#{name}"
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# A status not declared in .comat was returned.
|
|
22
|
+
class UnexpectedStatusError < Error
|
|
23
|
+
attr_reader :status, :body
|
|
24
|
+
|
|
25
|
+
def initialize(status, body)
|
|
26
|
+
@status = status
|
|
27
|
+
@body = body
|
|
28
|
+
super("想定していないステータス #{status} が返りました")
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Configuration error (a wrapper was not passed, no base url, etc.).
|
|
33
|
+
class ConfigError < Error; end
|
|
34
|
+
|
|
35
|
+
# The request failed.
|
|
36
|
+
class TransportError < Error; end
|
|
37
|
+
end
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Comatype
|
|
4
|
+
# A named string enum. Declare values with `values "admin", "member"` (constants are uppercase: Role::ADMIN).
|
|
5
|
+
class Enum
|
|
6
|
+
class << self
|
|
7
|
+
def values(*vals)
|
|
8
|
+
return @values if vals.empty?
|
|
9
|
+
|
|
10
|
+
@values = vals.freeze
|
|
11
|
+
vals.each { |v| const_set(v.to_s.upcase.gsub(/[^A-Z0-9]/, "_").sub(/\A(\d)/, 'V_\1'), v) }
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def decode(j, path = "")
|
|
15
|
+
raise ValidationError.new(path, "文字列にしてください(#{Codec.describe(j)} でした)") unless j.is_a?(String)
|
|
16
|
+
|
|
17
|
+
Check.one_of(path, j, values)
|
|
18
|
+
j
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def validate_value(v, path)
|
|
22
|
+
Check.one_of(path, v, values)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def encode_value(v)
|
|
26
|
+
v
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def from_string(s, name)
|
|
30
|
+
Check.one_of(name, s, values)
|
|
31
|
+
s
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def to_string(v)
|
|
35
|
+
v.to_s
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Object type. Subclasses declare `field :attr, "jsonName", type, optional: true`.
|
|
41
|
+
# Parent of generated types (named types, params / search / headers, body).
|
|
42
|
+
class Model
|
|
43
|
+
Field = Struct.new(:attr, :json, :type, :optional)
|
|
44
|
+
|
|
45
|
+
class << self
|
|
46
|
+
def fields
|
|
47
|
+
@fields ||= []
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def inherited(sub)
|
|
51
|
+
super
|
|
52
|
+
sub.instance_variable_set(:@fields, fields.dup)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def field(attr, json, type, optional: false)
|
|
56
|
+
fields << Field.new(attr, json, type, optional)
|
|
57
|
+
attr_accessor attr
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def nullable?(t)
|
|
61
|
+
t.is_a?(Array) && t[0] == :nullable
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# JSON tree -> instance
|
|
65
|
+
def decode(j, path = "")
|
|
66
|
+
raise ValidationError.new(path, "オブジェクトにしてください(#{Codec.describe(j)} でした)") unless j.is_a?(Hash)
|
|
67
|
+
|
|
68
|
+
kw = {}
|
|
69
|
+
fields.each do |f|
|
|
70
|
+
p = ValidationError.join(path, f.json)
|
|
71
|
+
if !j.key?(f.json) || (j[f.json].nil? && f.optional)
|
|
72
|
+
raise ValidationError.new(p, "必須です") unless f.optional || nullable?(f.type)
|
|
73
|
+
|
|
74
|
+
kw[f.attr] = nil
|
|
75
|
+
next
|
|
76
|
+
end
|
|
77
|
+
kw[f.attr] = Codec.decode(f.type, j[f.json], p)
|
|
78
|
+
end
|
|
79
|
+
new(**kw)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Build from path / query / header values (a call that returns name -> list of strings)
|
|
83
|
+
def from_wire(lookup, split: false)
|
|
84
|
+
kw = {}
|
|
85
|
+
fields.each do |f|
|
|
86
|
+
vals = lookup.call(f.json)
|
|
87
|
+
if vals.empty?
|
|
88
|
+
raise ValidationError.new(f.json, "必須です") unless f.optional || nullable?(f.type)
|
|
89
|
+
|
|
90
|
+
kw[f.attr] = nil
|
|
91
|
+
else
|
|
92
|
+
kw[f.attr] = Codec.from_wire(f.type, vals, f.json, split)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
new(**kw)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Entry point for validating nested values
|
|
99
|
+
def validate_value(v, path)
|
|
100
|
+
raise ValidationError.new(path, "#{name} にしてください") unless v.is_a?(self)
|
|
101
|
+
|
|
102
|
+
v.validate(path)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def encode_value(v)
|
|
106
|
+
v.encode
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Turn a hash (keyed by attr) or an existing instance into an instance of this class
|
|
110
|
+
def coerce(v)
|
|
111
|
+
return v if v.is_a?(self) || v.nil?
|
|
112
|
+
return new(**v.transform_keys(&:to_sym)) if v.is_a?(Hash)
|
|
113
|
+
|
|
114
|
+
raise ArgumentError, "#{name} かハッシュを渡してください(#{v.class})"
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def initialize(**kw)
|
|
119
|
+
unknown = kw.keys - self.class.fields.map(&:attr)
|
|
120
|
+
raise ArgumentError, "#{self.class.name} に無いキー: #{unknown.join(', ')}" unless unknown.empty?
|
|
121
|
+
|
|
122
|
+
self.class.fields.each { |f| instance_variable_set("@#{f.attr}", kw[f.attr]) }
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Check that it matches the .comat type (including constraints)
|
|
126
|
+
def validate(path = "")
|
|
127
|
+
self.class.fields.each do |f|
|
|
128
|
+
v = instance_variable_get("@#{f.attr}")
|
|
129
|
+
p = ValidationError.join(path, f.json)
|
|
130
|
+
if v.nil?
|
|
131
|
+
raise ValidationError.new(p, "必須です") unless f.optional || self.class.nullable?(f.type)
|
|
132
|
+
|
|
133
|
+
next
|
|
134
|
+
end
|
|
135
|
+
Codec.validate(f.type, v, p)
|
|
136
|
+
end
|
|
137
|
+
true
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# JSON tree (Hash with string keys). Validated before it is built
|
|
141
|
+
def encode
|
|
142
|
+
validate
|
|
143
|
+
self.class.fields.each_with_object({}) do |f, h|
|
|
144
|
+
v = instance_variable_get("@#{f.attr}")
|
|
145
|
+
next if v.nil? && f.optional
|
|
146
|
+
|
|
147
|
+
h[f.json] = Codec.encode(f.type, v)
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def to_h
|
|
152
|
+
self.class.fields.to_h { |f| [f.attr, instance_variable_get("@#{f.attr}")] }
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def ==(other)
|
|
156
|
+
other.class == self.class && other.to_h == to_h
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def to_s
|
|
160
|
+
"#<#{self.class.name} #{to_h.inspect}>"
|
|
161
|
+
end
|
|
162
|
+
alias inspect to_s
|
|
163
|
+
end
|
|
164
|
+
end
|
data/lib/comatype/raw.rb
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Comatype
|
|
4
|
+
# Case-insensitive headers (the same name may appear several times).
|
|
5
|
+
class Headers
|
|
6
|
+
include Enumerable
|
|
7
|
+
|
|
8
|
+
def initialize
|
|
9
|
+
@items = []
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def [](name)
|
|
13
|
+
kv = @items.find { |k, _| k.casecmp?(name) }
|
|
14
|
+
kv&.last
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def get_all(name)
|
|
18
|
+
@items.select { |k, _| k.casecmp?(name) }.map(&:last)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Replace entries with the same name
|
|
22
|
+
def []=(name, value)
|
|
23
|
+
@items.reject! { |k, _| k.casecmp?(name) }
|
|
24
|
+
@items << [name, value.to_s]
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Add, keeping entries with the same name
|
|
28
|
+
def add(name, value)
|
|
29
|
+
@items << [name, value.to_s]
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def key?(name)
|
|
33
|
+
!self[name].nil?
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def each(&block)
|
|
37
|
+
@items.each(&block)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# The raw request a wrapper sees (the on-the-wire form, with no per-endpoint types).
|
|
42
|
+
class RawRequest
|
|
43
|
+
# method: lowercase, path: with params filled in (client) / the path that arrived (server)
|
|
44
|
+
attr_accessor :method, :path, :headers, :query, :params, :body, :cookies, :ctx
|
|
45
|
+
|
|
46
|
+
def initialize(method, path)
|
|
47
|
+
@method = method
|
|
48
|
+
@path = path
|
|
49
|
+
@headers = Headers.new
|
|
50
|
+
@query = []
|
|
51
|
+
@params = {}
|
|
52
|
+
@body = "".b
|
|
53
|
+
@cookies = {}
|
|
54
|
+
@ctx = {} # values added by server wrappers (wrapper name -> value)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def cookie(name)
|
|
58
|
+
@cookies[name.to_s]
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def query_all(name)
|
|
62
|
+
@query.select { |k, _| k == name }.map(&:last)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# The raw response a wrapper sees (the on-the-wire form).
|
|
67
|
+
class RawResponse
|
|
68
|
+
# set_cookies: Set-Cookie lines to send (server) / lines received (client), err: a cause not included in the response
|
|
69
|
+
attr_accessor :status, :headers, :body, :set_cookies, :err
|
|
70
|
+
|
|
71
|
+
def initialize(status)
|
|
72
|
+
@status = status
|
|
73
|
+
@headers = Headers.new
|
|
74
|
+
@body = "".b
|
|
75
|
+
@set_cookies = []
|
|
76
|
+
@err = nil
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def self.json(status, tree)
|
|
80
|
+
r = new(status)
|
|
81
|
+
unless tree.nil?
|
|
82
|
+
r.body = JSON.generate(tree).b
|
|
83
|
+
r.headers["Content-Type"] = "application/json"
|
|
84
|
+
end
|
|
85
|
+
r
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Turn the server's own error into a 500. Details are not shown to the client.
|
|
89
|
+
def self.error(cause)
|
|
90
|
+
r = new(500)
|
|
91
|
+
r.body = '{"code":"internal_error","message":"Internal Server Error"}'.b
|
|
92
|
+
r.headers["Content-Type"] = "application/json"
|
|
93
|
+
r.err = cause.to_s
|
|
94
|
+
r
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Comatype
|
|
4
|
+
# Parent of the per-status responses. Generated code declares `class S200 < Comatype::Response`.
|
|
5
|
+
# status 200 / body_type <type description> / headers_class <Model subclass>
|
|
6
|
+
# The server returns `S200.new(body, headers: {...})`; the client receives an instance of this class.
|
|
7
|
+
class Response
|
|
8
|
+
class << self
|
|
9
|
+
def status(n = nil)
|
|
10
|
+
n.nil? ? @status : (@status = n)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def body_type(t = nil)
|
|
14
|
+
t.nil? ? @body_type : (@body_type = t)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def headers_class(k = nil)
|
|
18
|
+
k.nil? ? @headers_class : (@headers_class = k)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Build the RawResponse a wrapper returns (no headers / cookies). Used by the generated `Init.s401(body)`
|
|
22
|
+
def raw(body = nil)
|
|
23
|
+
out = RawResponse.new(status)
|
|
24
|
+
if body_type
|
|
25
|
+
Codec.validate(body_type, body, "")
|
|
26
|
+
out.body = JSON.generate(Codec.encode(body_type, body)).b
|
|
27
|
+
out.headers["Content-Type"] = "application/json"
|
|
28
|
+
end
|
|
29
|
+
out
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
attr_reader :body, :headers, :cookie
|
|
34
|
+
|
|
35
|
+
# A status without a body is `S204.new`; headers only is `S200.new(headers: {...})`
|
|
36
|
+
def initialize(body = nil, headers: nil)
|
|
37
|
+
if body.nil? && self.class.body_type && !(self.class.body_type.is_a?(Array) && self.class.body_type[0] == :nullable)
|
|
38
|
+
raise ArgumentError, "#{self.class.name} にはボディが要ります"
|
|
39
|
+
end
|
|
40
|
+
raise ArgumentError, "#{self.class.name} にはボディを渡せません" if !body.nil? && self.class.body_type.nil?
|
|
41
|
+
|
|
42
|
+
@body = body
|
|
43
|
+
hc = self.class.headers_class
|
|
44
|
+
raise ArgumentError, "#{self.class.name} にはヘッダを渡せません" if headers && hc.nil?
|
|
45
|
+
|
|
46
|
+
@headers = hc ? (hc.coerce(headers) || hc.new) : nil
|
|
47
|
+
@cookie = nil
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def status
|
|
51
|
+
self.class.status
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Issue a cookie from init.comat (server)
|
|
55
|
+
def with_cookie(value, max_age: nil, path: nil, domain: nil)
|
|
56
|
+
@cookie = CookieValue.new(value: value, delete: false, max_age: max_age, path: path, domain: domain)
|
|
57
|
+
self
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Delete a cookie (server)
|
|
61
|
+
def clear_cookie
|
|
62
|
+
@cookie = CookieValue.new(value: "", delete: true)
|
|
63
|
+
self
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def to_s
|
|
67
|
+
"#<#{self.class.name} body=#{@body.inspect} headers=#{@headers.inspect}>"
|
|
68
|
+
end
|
|
69
|
+
alias inspect to_s
|
|
70
|
+
end
|
|
71
|
+
end
|