Class: Nfe::Resources::AbstractResource
- Inherits:
-
Object
- Object
- Nfe::Resources::AbstractResource
- Defined in:
- lib/nfe/resources/abstract_resource.rb,
sig/nfe/resources/abstract_resource.rbs
Overview
Base class for every SDK resource. Constructed with the owning
Client, it provides the HTTP verb helpers (+get+/+post+/+put+/
delete) routed through the client's transport for the resource's declared
api_family, plus hydration and async-response helpers shared by every
resource.
Subclasses declare their api_family (and optionally override
api_version, which defaults to "v1"; the addresses resource returns
"" because its host already embeds /v2). Business methods are added by
the resource changes (add-invoice-resources, add-entity-resources, ...).
All HTTP helpers accept an optional request_options: (Nfe::RequestOptions)
that is threaded through to Client#request for per-call multi-tenant
overrides.
Direct Known Subclasses
Addresses, Companies, ConsumerInvoiceQuery, ConsumerInvoices, InboundProductInvoices, LegalEntityLookup, LegalPeople, NaturalPeople, NaturalPersonLookup, ProductInvoiceQuery, ProductInvoices, ProductInvoicesRtc, ServiceInvoices, ServiceInvoicesRtc, StateTaxes, TaxCalculation, TaxCodes, TransportationInvoices, Webhooks
Instance Attribute Summary collapse
-
#client ⇒ Nfe::Client
readonly
The owning client.
Instance Method Summary collapse
-
#api_family ⇒ Symbol
The product family this resource belongs to (e.g.
:main,:cte). -
#api_version ⇒ String
The path version segment prefixed to every request path.
-
#build_list_page(payload) ⇒ Nfe::ListPage
Detect the pagination shape of
payloadand build the matching ListPage. -
#build_multipart_body(parts, boundary) ⇒ String
Assemble the raw multipart/form-data body for
partsunderboundary. - #cursor_list_page(payload, total) ⇒ Nfe::ListPage
-
#delete(path, query: {}, request_options: nil, headers: {}) ⇒ Nfe::Http::Response
Issue a DELETE request for this resource's family.
-
#dig_key(payload, camel_key, snake_key) ⇒ Object
Reads a value under either the camelCase (String) or snake_case (Symbol) key.
-
#download(path, query: {}, request_options: nil, headers: {}) ⇒ String
Perform a GET and return the response body as binary-safe bytes (+ASCII-8BIT+), so binary documents (PDF/XML/ZIP) are not corrupted.
-
#extract_invoice_id(location) ⇒ String?
Extract the trailing id from a
Locationpath, e.g. -
#full_path(path) ⇒ String
Prefix
pathwith/#{api_version}unless the version is empty (in which casepathis returned unchanged, avoiding a doubled slash). -
#get(path, query: {}, request_options: nil, headers: {}) ⇒ Nfe::Http::Response
Issue a GET request for this resource's family.
-
#handle_async_response(response, issued_klass:) ⇒ Nfe::Pending, Nfe::Issued
Interpret an emission response.
-
#hydrate(klass, payload) ⇒ Object
Materialize a generated DTO by delegating to its
from_apifactory (camelCase -> snake_case, unknown-key drop, nested recursion). -
#hydrate_list(klass, payload, wrapper_key:) ⇒ Nfe::ListResponse
Unwrap
payload[wrapper_key], hydrate each item withklass, and build an ListResponse whosepagereflects the pagination shape: page-style (+page_index+/+page_count+) or cursor-style (+starting_after+/+ending_before+). -
#initialize(client) ⇒ AbstractResource
constructor
A new instance of AbstractResource.
-
#multipart_part(name, value) ⇒ String
Encode a single multipart part (scalar field or file).
- #page_list_page(page_index, page_count, total) ⇒ Nfe::ListPage
-
#parse_json(body) ⇒ Object
Parse a JSON body, tolerating nil/empty/malformed bodies.
-
#post(path, body: nil, query: {}, request_options: nil, headers: {}, idempotency_key: nil) ⇒ Nfe::Http::Response
Issue a POST request for this resource's family.
-
#put(path, body: nil, query: {}, request_options: nil, headers: {}) ⇒ Nfe::Http::Response
Issue a PUT request for this resource's family.
-
#unwrap(payload, key) ⇒ Object
Unwrap an API envelope: return
payload[key]when present (String or Symbol key), otherwise returnpayloadunchanged. -
#upload_multipart(path, parts) ⇒ Nfe::Http::Response
POST a
multipart/form-databody built frompartsusing only Ruby stdlib, introducing no new runtime dependency.
Constructor Details
#initialize(client) ⇒ AbstractResource
Returns a new instance of AbstractResource.
23 24 25 |
# File 'lib/nfe/resources/abstract_resource.rb', line 23 def initialize(client) @client = client end |
Instance Attribute Details
#client ⇒ Nfe::Client (readonly)
Returns the owning client.
30 31 32 |
# File 'lib/nfe/resources/abstract_resource.rb', line 30 def client @client end |
Instance Method Details
#api_family ⇒ Symbol
The product family this resource belongs to (e.g. :main, :cte).
Subclasses MUST override.
36 37 38 |
# File 'lib/nfe/resources/abstract_resource.rb', line 36 def api_family raise NotImplementedError, "#{self.class} must declare an api_family" end |
#api_version ⇒ String
The path version segment prefixed to every request path. Defaults to
"v1"; a resource whose host already embeds the version (addresses)
overrides this to "".
45 46 47 |
# File 'lib/nfe/resources/abstract_resource.rb', line 45 def api_version "v1" end |
#build_list_page(payload) ⇒ Nfe::ListPage
Detect the pagination shape of payload and build the matching
ListPage. Page-style takes precedence when paging fields exist.
156 157 158 159 160 161 162 163 164 |
# File 'lib/nfe/resources/abstract_resource.rb', line 156 def build_list_page(payload) page_index = dig_key(payload, "pageIndex", :page_index) page_count = dig_key(payload, "pageCount", :page_count) total = payload["totalResults"] || dig_key(payload, "total", :total) return page_list_page(page_index, page_count, total) if page_index || page_count cursor_list_page(payload, total) end |
#build_multipart_body(parts, boundary) ⇒ String
Assemble the raw multipart/form-data body for parts under boundary.
238 239 240 241 242 243 244 245 246 |
# File 'lib/nfe/resources/abstract_resource.rb', line 238 def build_multipart_body(parts, boundary) buffer = String.new(encoding: Encoding::ASCII_8BIT) parts.each do |name, value| buffer << "--#{boundary}\r\n".b buffer << multipart_part(name.to_s, value) end buffer << "--#{boundary}--\r\n".b buffer end |
#cursor_list_page(payload, total) ⇒ Nfe::ListPage
175 176 177 178 179 180 181 |
# File 'lib/nfe/resources/abstract_resource.rb', line 175 def cursor_list_page(payload, total) starting_after = dig_key(payload, "startingAfter", :starting_after) ending_before = dig_key(payload, "endingBefore", :ending_before) return Nfe::ListPage.new(total: total) unless starting_after || ending_before Nfe::ListPage.new(starting_after: starting_after, ending_before: ending_before, total: total) end |
#delete(path, query: {}, request_options: nil, headers: {}) ⇒ Nfe::Http::Response
Issue a DELETE request for this resource's family.
73 74 75 76 77 |
# File 'lib/nfe/resources/abstract_resource.rb', line 73 def delete(path, query: {}, request_options: nil, headers: {}) client.request(:delete, family: api_family, path: full_path(path), query: query, headers: headers, request_options: ) end |
#dig_key(payload, camel_key, snake_key) ⇒ Object
Reads a value under either the camelCase (String) or snake_case (Symbol) key.
167 168 169 |
# File 'lib/nfe/resources/abstract_resource.rb', line 167 def dig_key(payload, camel_key, snake_key) payload[camel_key] || payload[snake_key] end |
#download(path, query: {}, request_options: nil, headers: {}) ⇒ String
Perform a GET and return the response body as binary-safe bytes (+ASCII-8BIT+), so binary documents (PDF/XML/ZIP) are not corrupted.
106 107 108 109 110 |
# File 'lib/nfe/resources/abstract_resource.rb', line 106 def download(path, query: {}, request_options: nil, headers: {}) response = get(path, query: query, request_options: , headers: headers) (response.body || "").dup.force_encoding(Encoding::ASCII_8BIT) end |
#extract_invoice_id(location) ⇒ String?
Extract the trailing id from a Location path, e.g.
/v1/companies/x/serviceinvoices/abc-123 -> "abc-123".
185 186 187 188 |
# File 'lib/nfe/resources/abstract_resource.rb', line 185 def extract_invoice_id(location) match = location.match(%r{/([a-z0-9-]+)\z}i) match ? match[1] : nil end |
#full_path(path) ⇒ String
Prefix path with /#{api_version} unless the version is empty (in
which case path is returned unchanged, avoiding a doubled slash).
84 85 86 87 88 89 |
# File 'lib/nfe/resources/abstract_resource.rb', line 84 def full_path(path) version = api_version.to_s return path if version.empty? "/#{version}#{path}" end |
#get(path, query: {}, request_options: nil, headers: {}) ⇒ Nfe::Http::Response
Issue a GET request for this resource's family.
50 51 52 53 54 |
# File 'lib/nfe/resources/abstract_resource.rb', line 50 def get(path, query: {}, request_options: nil, headers: {}) client.request(:get, family: api_family, path: full_path(path), query: query, headers: headers, request_options: ) end |
#handle_async_response(response, issued_klass:) ⇒ Nfe::Pending, Nfe::Issued
Interpret an emission response. A 202 with a Location header yields an
Pending (its invoice_id parsed from the final path segment); a
202 without Location is a protocol violation raising
InvoiceProcessingError. A 201/200 hydrates issued_klass from the
JSON body and yields an Issued.
137 138 139 140 141 142 143 144 145 146 147 148 149 150 |
# File 'lib/nfe/resources/abstract_resource.rb', line 137 def handle_async_response(response, issued_klass:) if response.status == 202 location = response.location if location.nil? || location.empty? raise Nfe::InvoiceProcessingError.new( "Resposta 202 sem cabeçalho Location: não é possível identificar a nota em processamento.", status_code: response.status, response_headers: response.headers ) end Nfe::Pending.new(invoice_id: extract_invoice_id(location), location: location) else Nfe::Issued.new(resource: hydrate(issued_klass, parse_json(response.body))) end end |
#hydrate(klass, payload) ⇒ Object
Materialize a generated DTO by delegating to its from_api factory
(camelCase -> snake_case, unknown-key drop, nested recursion). The
factory itself is produced by add-openapi-pipeline; this is only the
call site.
98 99 100 |
# File 'lib/nfe/resources/abstract_resource.rb', line 98 def hydrate(klass, payload) klass.from_api(payload) end |
#hydrate_list(klass, payload, wrapper_key:) ⇒ Nfe::ListResponse
Unwrap payload[wrapper_key], hydrate each item with klass, and build
an ListResponse whose page reflects the pagination shape:
page-style (+page_index+/+page_count+) or cursor-style
(+starting_after+/+ending_before+).
121 122 123 124 125 126 |
# File 'lib/nfe/resources/abstract_resource.rb', line 121 def hydrate_list(klass, payload, wrapper_key:) payload ||= {} #: Hash[untyped, untyped] raw_items = payload[wrapper_key.to_s] || payload[wrapper_key.to_sym] || [] items = raw_items.map { |item| hydrate(klass, item) } Nfe::ListResponse.new(data: items, page: build_list_page(payload)) end |
#multipart_part(name, value) ⇒ String
Encode a single multipart part (scalar field or file).
249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 |
# File 'lib/nfe/resources/abstract_resource.rb', line 249 def multipart_part(name, value) part = String.new(encoding: Encoding::ASCII_8BIT) if value.is_a?(Hash) filename = value[:filename] || value["filename"] || "file" content_type = value[:content_type] || value["content_type"] || "application/octet-stream" content = value[:content] || value["content"] || "" part << %(Content-Disposition: form-data; name="#{name}"; filename="#{filename}"\r\n).b part << "Content-Type: #{content_type}\r\n\r\n".b part << content.dup.force_encoding(Encoding::ASCII_8BIT) else part << %(Content-Disposition: form-data; name="#{name}"\r\n\r\n).b part << value.to_s.dup.force_encoding(Encoding::ASCII_8BIT) end part << "\r\n".b part end |
#page_list_page(page_index, page_count, total) ⇒ Nfe::ListPage
171 172 173 |
# File 'lib/nfe/resources/abstract_resource.rb', line 171 def page_list_page(page_index, page_count, total) Nfe::ListPage.new(page_index: page_index, page_count: page_count, total: total) end |
#parse_json(body) ⇒ Object
Parse a JSON body, tolerating nil/empty/malformed bodies.
191 192 193 194 195 196 197 |
# File 'lib/nfe/resources/abstract_resource.rb', line 191 def parse_json(body) return nil if body.nil? || body.empty? JSON.parse(body) rescue JSON::ParserError nil end |
#post(path, body: nil, query: {}, request_options: nil, headers: {}, idempotency_key: nil) ⇒ Nfe::Http::Response
Issue a POST request for this resource's family.
57 58 59 60 61 62 63 |
# File 'lib/nfe/resources/abstract_resource.rb', line 57 def post(path, body: nil, query: {}, request_options: nil, headers: {}, idempotency_key: nil) client.request(:post, family: api_family, path: full_path(path), query: query, body: body, headers: headers, idempotency_key: idempotency_key, request_options: ) end |
#put(path, body: nil, query: {}, request_options: nil, headers: {}) ⇒ Nfe::Http::Response
Issue a PUT request for this resource's family.
66 67 68 69 70 |
# File 'lib/nfe/resources/abstract_resource.rb', line 66 def put(path, body: nil, query: {}, request_options: nil, headers: {}) client.request(:put, family: api_family, path: full_path(path), query: query, body: body, headers: headers, request_options: ) end |
#unwrap(payload, key) ⇒ Object
Unwrap an API envelope: return payload[key] when present (String or
Symbol key), otherwise return payload unchanged. Tolerant of a missing
envelope so callers can pass a wrapped or already-unwrapped Hash.
206 207 208 209 210 211 212 213 214 215 216 |
# File 'lib/nfe/resources/abstract_resource.rb', line 206 def unwrap(payload, key) return payload unless payload.is_a?(Hash) if payload.key?(key.to_s) payload[key.to_s] elsif payload.key?(key.to_sym) payload[key.to_sym] else payload end end |
#upload_multipart(path, parts) ⇒ Nfe::Http::Response
POST a multipart/form-data body built from parts using only Ruby
stdlib, introducing no new runtime dependency. Each part is either a
scalar field value (+String+) or a file part described by a Hash with
:filename, :content, and optional :content_type.
The body is assembled as binary (+ASCII-8BIT+) so certificate bytes are
never corrupted, and the Content-Type header carries the generated
boundary. The multipart body is intentionally never logged.
230 231 232 233 234 235 |
# File 'lib/nfe/resources/abstract_resource.rb', line 230 def upload_multipart(path, parts) boundary = "----NfeBoundary#{parts.object_id}" body = build_multipart_body(parts, boundary) headers = { "Content-Type" => "multipart/form-data; boundary=#{boundary}" } post(path, body: body, headers: headers) end |