buezli-api 0.1.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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +107 -0
- data/lib/buezli/api/client/download.rb +16 -0
- data/lib/buezli/api/client/error.rb +36 -0
- data/lib/buezli/api/client/list.rb +60 -0
- data/lib/buezli/api/client/record.rb +78 -0
- data/lib/buezli/api/client.rb +168 -0
- data/lib/buezli/api/schema.rb +57 -0
- data/lib/buezli/api/version.rb +5 -0
- data/lib/buezli/api.rb +18 -0
- data/schemas/2026-07-14.json +563 -0
- metadata +101 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: '094aa7cb35ecc313611cd66a86c777431461d8a8c8fdc9e6508f0a47d54bf5f5'
|
|
4
|
+
data.tar.gz: e0750a2351cf57f9fe1911373c41fc4a04946da03ef24fbd0dc76225af289122
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 50b99f0e3d1aa8d1a9d5525f9aacbcb763347da6ad95835ea523794dfcb6020c22d72dd1bf812fb995142c4fa80342f51d5f5cf4483866cd2d2a3b2aab92bb91
|
|
7
|
+
data.tar.gz: 97e5011a61ac35e4867fccbbf9e022435ad2636b31bd7c3888090942c36bd198a996e2a92eba9050dfe560a1531e519b5b5d84a136d509bc296090fd1645edec
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Codegestalt
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# buezli-api
|
|
2
|
+
|
|
3
|
+
The official schema-driven Ruby client for the read-only
|
|
4
|
+
[Buezli API](https://buezli.app/api). Buezli is straightforward invoicing,
|
|
5
|
+
time tracking, and project management for small service teams. Learn more at
|
|
6
|
+
[buezli.app](https://buezli.app).
|
|
7
|
+
|
|
8
|
+
The gem builds its methods from a bundled, date-versioned API schema. It
|
|
9
|
+
supports records, expansions, delta polling, pagination, PDFs, and receipts.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```ruby
|
|
14
|
+
gem "buezli-api"
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
`buezli-api` requires Ruby 3.2 or newer.
|
|
18
|
+
|
|
19
|
+
## Quick start
|
|
20
|
+
|
|
21
|
+
```ruby
|
|
22
|
+
require "buezli/api"
|
|
23
|
+
|
|
24
|
+
client = Buezli::Api::Client.new(
|
|
25
|
+
access_token: ENV.fetch("BUEZLI_API_TOKEN")
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
invoices = client.retrieve_invoices(limit: 50)
|
|
29
|
+
invoice = client.retrieve_invoice(123, expand: ["client"])
|
|
30
|
+
|
|
31
|
+
invoice.id
|
|
32
|
+
invoice.issue_date # Date
|
|
33
|
+
invoice.updated_at # Time
|
|
34
|
+
invoice.total # { "cents" => 10810, "currency" => "CHF" }
|
|
35
|
+
invoice.client.name
|
|
36
|
+
invoice.line_items.first.description
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
The default endpoint is `https://api.buezli.app`, and the newest bundled
|
|
40
|
+
schema is selected automatically. Pin an API version when needed:
|
|
41
|
+
|
|
42
|
+
```ruby
|
|
43
|
+
client = Buezli::Api::Client.new(
|
|
44
|
+
access_token: ENV.fetch("BUEZLI_API_TOKEN"),
|
|
45
|
+
version: "2026-07-14"
|
|
46
|
+
)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Schema-declared dates and timestamps become `Date` and `Time`. Decimal values
|
|
50
|
+
remain strings and money remains a `cents`/`currency` hash, avoiding precision
|
|
51
|
+
loss. Unknown record methods raise `NoMethodError`; declared but absent fields
|
|
52
|
+
return `nil`.
|
|
53
|
+
|
|
54
|
+
## Pagination and deltas
|
|
55
|
+
|
|
56
|
+
Collections are enumerable and retain the original query when paging:
|
|
57
|
+
|
|
58
|
+
```ruby
|
|
59
|
+
invoices = client.retrieve_invoices(
|
|
60
|
+
updated_since: Time.utc(2026, 7, 14, 10),
|
|
61
|
+
expand: ["client"],
|
|
62
|
+
page: 1,
|
|
63
|
+
limit: 100
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
invoices.each do |invoice|
|
|
67
|
+
invoice.deleted? ? remove_local(invoice.id) : replace_local(invoice.to_h)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
invoices.pagination
|
|
71
|
+
next_page = invoices.next_page
|
|
72
|
+
previous_page = next_page&.previous_page
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Downloads
|
|
76
|
+
|
|
77
|
+
```ruby
|
|
78
|
+
pdf = client.retrieve_invoice_pdf(123)
|
|
79
|
+
receipt = client.retrieve_expense_receipt(456)
|
|
80
|
+
|
|
81
|
+
pdf.body
|
|
82
|
+
pdf.filename
|
|
83
|
+
pdf.content_type
|
|
84
|
+
pdf.headers
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Save a download when needed:
|
|
88
|
+
|
|
89
|
+
```ruby
|
|
90
|
+
File.binwrite(pdf.filename, pdf.body)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Schema and errors
|
|
94
|
+
|
|
95
|
+
Inspect the selected API schema with `client.schema`, or fetch the schema
|
|
96
|
+
currently served by Buezli with `client.retrieve_schema`.
|
|
97
|
+
|
|
98
|
+
Failed requests raise `Buezli::Api::Client::Error`, exposing `status`, `code`,
|
|
99
|
+
`message`, `details`, `response_body`, and `headers`. Access tokens are never
|
|
100
|
+
included in errors or inspection output.
|
|
101
|
+
|
|
102
|
+
See the [API documentation](https://buezli.app/api) for available resources,
|
|
103
|
+
fields, expansions, and authentication.
|
|
104
|
+
|
|
105
|
+
## License
|
|
106
|
+
|
|
107
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
module Buezli
|
|
2
|
+
module Api
|
|
3
|
+
class Client
|
|
4
|
+
class Download
|
|
5
|
+
attr_reader :body, :filename, :content_type, :headers
|
|
6
|
+
|
|
7
|
+
def initialize(body:, filename:, content_type:, headers:)
|
|
8
|
+
@body = body
|
|
9
|
+
@filename = filename
|
|
10
|
+
@content_type = content_type
|
|
11
|
+
@headers = headers
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
module Buezli
|
|
2
|
+
module Api
|
|
3
|
+
class Client
|
|
4
|
+
class Error < StandardError
|
|
5
|
+
attr_reader :status, :code, :details, :response_body, :headers
|
|
6
|
+
|
|
7
|
+
def initialize(response)
|
|
8
|
+
if response.is_a?(String)
|
|
9
|
+
@details = {}
|
|
10
|
+
@headers = {}.with_indifferent_access
|
|
11
|
+
return super(response)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
@status = response.code.to_i
|
|
15
|
+
@response_body = response.body.to_s
|
|
16
|
+
@headers = response.each_header.to_h.with_indifferent_access
|
|
17
|
+
parsed = parse_json(response_body)
|
|
18
|
+
@code = parsed&.dig("error", "code")
|
|
19
|
+
@details = parsed&.dig("error", "details") || {}
|
|
20
|
+
message = parsed&.dig("error", "message") || response_body.presence ||
|
|
21
|
+
"#{response.code} #{response.message}"
|
|
22
|
+
|
|
23
|
+
super(message)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def parse_json(body)
|
|
29
|
+
JSON.parse(body)
|
|
30
|
+
rescue JSON::ParserError, TypeError
|
|
31
|
+
nil
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
module Buezli
|
|
2
|
+
module Api
|
|
3
|
+
class Client
|
|
4
|
+
class List
|
|
5
|
+
include Enumerable
|
|
6
|
+
|
|
7
|
+
attr_reader :data, :meta
|
|
8
|
+
|
|
9
|
+
def initialize(data:, meta:, client:, request_context:)
|
|
10
|
+
@data = data
|
|
11
|
+
@meta = (meta || {}).with_indifferent_access
|
|
12
|
+
@client = client
|
|
13
|
+
@request_context = request_context
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def each(&block)
|
|
17
|
+
data.each(&block)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def empty?
|
|
21
|
+
data.empty?
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def size
|
|
25
|
+
data.size
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
alias length size
|
|
29
|
+
|
|
30
|
+
def [](index)
|
|
31
|
+
data[index]
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def pagination
|
|
35
|
+
meta.fetch(:pagination, {})
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def next_page
|
|
39
|
+
request_page(pagination[:next])
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def previous_page
|
|
43
|
+
request_page(pagination[:prev])
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def request_page(page)
|
|
49
|
+
return if page.nil?
|
|
50
|
+
|
|
51
|
+
current_page = pagination[:page] || @request_context[:params][:page]
|
|
52
|
+
raise Error, "Infinite pagination loop detected" if current_page.to_s == page.to_s
|
|
53
|
+
|
|
54
|
+
params = @request_context[:params].merge(page:)
|
|
55
|
+
@client.send(:perform_request, **@request_context.merge(params:))
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
module Buezli
|
|
2
|
+
module Api
|
|
3
|
+
class Client
|
|
4
|
+
class Record
|
|
5
|
+
attr_reader :raw_attributes, :model_name, :client, :schema
|
|
6
|
+
|
|
7
|
+
def initialize(attributes:, model_name:, client:, schema:)
|
|
8
|
+
@raw_attributes = (attributes || {}).with_indifferent_access
|
|
9
|
+
@model_name = model_name
|
|
10
|
+
@client = client
|
|
11
|
+
@schema = schema
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def id
|
|
15
|
+
raw_attributes[:id]
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def deleted?
|
|
19
|
+
raw_attributes[:_deleted] == true
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def [](key)
|
|
23
|
+
raw_attributes[key]
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def to_h
|
|
27
|
+
raw_attributes.to_h
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def method_missing(method, *args, &block)
|
|
31
|
+
return super unless args.empty? && block.nil? && schema.declared_field?(model_name, method)
|
|
32
|
+
|
|
33
|
+
read_value(method.to_s)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def respond_to_missing?(method, include_private = false)
|
|
37
|
+
schema.declared_field?(model_name, method) || super
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def read_value(name)
|
|
43
|
+
value = raw_attributes[name]
|
|
44
|
+
return build_association(name, value) if schema.association?(model_name, name)
|
|
45
|
+
|
|
46
|
+
case schema.attribute_type(model_name, name).to_s
|
|
47
|
+
when "datetime" then parse_time(value)
|
|
48
|
+
when "date" then parse_date(value)
|
|
49
|
+
else value
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def build_association(name, value)
|
|
54
|
+
return if value.nil?
|
|
55
|
+
|
|
56
|
+
target_model = schema.association_class_name(model_name, name)
|
|
57
|
+
if schema.association_collection?(model_name, name)
|
|
58
|
+
Array(value).map { |attributes| client.build_record(target_model, attributes) }
|
|
59
|
+
else
|
|
60
|
+
client.build_record(target_model, value)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def parse_time(value)
|
|
65
|
+
Time.parse(value.to_s) if value.present?
|
|
66
|
+
rescue ArgumentError
|
|
67
|
+
value
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def parse_date(value)
|
|
71
|
+
Date.parse(value.to_s) if value.present?
|
|
72
|
+
rescue Date::Error
|
|
73
|
+
value
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
module Buezli
|
|
2
|
+
module Api
|
|
3
|
+
class Client
|
|
4
|
+
DEFAULT_ENDPOINT = "https://api.buezli.app"
|
|
5
|
+
|
|
6
|
+
attr_reader :access_token, :endpoint, :version, :schema, :host
|
|
7
|
+
|
|
8
|
+
def initialize(access_token:, endpoint: DEFAULT_ENDPOINT, version: nil, schema: nil, host: nil)
|
|
9
|
+
@access_token = access_token
|
|
10
|
+
@endpoint = endpoint.chomp("/")
|
|
11
|
+
@schema = schema || load_schema(version)
|
|
12
|
+
@version = version || @schema.version
|
|
13
|
+
@host = host
|
|
14
|
+
raise ArgumentError, "Schema version does not match requested version" if @schema.version != @version
|
|
15
|
+
|
|
16
|
+
define_methods_from_schema
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def build_record(model_name, attributes)
|
|
20
|
+
Record.new(attributes:, model_name:, client: self, schema:)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def inspect
|
|
24
|
+
"#<#{self.class.name} endpoint=#{endpoint.inspect} version=#{version.inspect} host=#{host.inspect}>"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def load_schema(version)
|
|
30
|
+
requested_version = version || latest_schema_version
|
|
31
|
+
Schema.load(schema_directory.join("#{requested_version}.json"))
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def schema_directory
|
|
35
|
+
Pathname(__dir__).join("../../../schemas").expand_path
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def latest_schema_version
|
|
39
|
+
schema_directory.glob("*.json").map { |path| path.basename(".json").to_s }.max ||
|
|
40
|
+
raise(ArgumentError, "No bundled API schemas found")
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def define_methods_from_schema
|
|
44
|
+
schema.endpoints.each do |resource, endpoints|
|
|
45
|
+
endpoints.each do |endpoint_config|
|
|
46
|
+
define_singleton_method(method_name_for(resource, endpoint_config.fetch("action"))) do |*path_values, **params|
|
|
47
|
+
perform_endpoint(resource, endpoint_config, path_values, params)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def method_name_for(resource, action)
|
|
54
|
+
return "retrieve_#{resource}" if action == "index"
|
|
55
|
+
return "retrieve_#{resource.singularize}" if action == "show"
|
|
56
|
+
|
|
57
|
+
"retrieve_#{resource.singularize}_#{action}"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def perform_endpoint(resource, endpoint_config, path_values, params)
|
|
61
|
+
path = endpoint_config.fetch("path").dup
|
|
62
|
+
query = params.with_indifferent_access
|
|
63
|
+
|
|
64
|
+
path.scan(/:(\w+)/).flatten.each do |key|
|
|
65
|
+
value = query.delete(key) || path_values.shift
|
|
66
|
+
raise ArgumentError, "Missing required path parameter: #{key}" if value.nil?
|
|
67
|
+
|
|
68
|
+
path.sub!(":#{key}", value.to_s)
|
|
69
|
+
end
|
|
70
|
+
raise ArgumentError, "Too many path parameters" if path_values.any?
|
|
71
|
+
|
|
72
|
+
perform_request(
|
|
73
|
+
resource:,
|
|
74
|
+
action: endpoint_config.fetch("action"),
|
|
75
|
+
verb: endpoint_config.fetch("verb"),
|
|
76
|
+
path:,
|
|
77
|
+
params: query
|
|
78
|
+
)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def perform_request(resource:, action:, verb:, path:, params:)
|
|
82
|
+
raise ArgumentError, "Unsupported API verb: #{verb}" unless verb == "GET"
|
|
83
|
+
|
|
84
|
+
uri = URI("#{endpoint}#{path}")
|
|
85
|
+
uri.query = encode_query(params) if params.present?
|
|
86
|
+
request = Net::HTTP::Get.new(uri)
|
|
87
|
+
request["Authorization"] = "Bearer #{access_token}"
|
|
88
|
+
request["Accept"] = "application/json, */*"
|
|
89
|
+
request["Host"] = host if host
|
|
90
|
+
|
|
91
|
+
response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") do |http|
|
|
92
|
+
http.request(request)
|
|
93
|
+
end
|
|
94
|
+
raise Error.new(response) unless response.is_a?(Net::HTTPSuccess)
|
|
95
|
+
|
|
96
|
+
parse_response(
|
|
97
|
+
response,
|
|
98
|
+
resource:,
|
|
99
|
+
request_context: { resource:, action:, verb:, path:, params: params.dup }
|
|
100
|
+
)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def parse_response(response, resource:, request_context:)
|
|
104
|
+
content_type = response["Content-Type"].to_s.split(";").first
|
|
105
|
+
return build_download(response) unless content_type == "application/json" || content_type.end_with?("+json")
|
|
106
|
+
|
|
107
|
+
payload = JSON.parse(response.body)
|
|
108
|
+
return Schema.new(payload) if resource == "schema"
|
|
109
|
+
|
|
110
|
+
data = payload.key?("data") ? payload["data"] : payload
|
|
111
|
+
model_name = schema.model_name_for_resource(resource)
|
|
112
|
+
return payload unless model_name
|
|
113
|
+
|
|
114
|
+
if data.is_a?(Array)
|
|
115
|
+
List.new(
|
|
116
|
+
data: data.map { |attributes| build_record(model_name, attributes) },
|
|
117
|
+
meta: payload["meta"],
|
|
118
|
+
client: self,
|
|
119
|
+
request_context:
|
|
120
|
+
)
|
|
121
|
+
elsif data.is_a?(Hash)
|
|
122
|
+
build_record(model_name, data)
|
|
123
|
+
else
|
|
124
|
+
data
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def build_download(response)
|
|
129
|
+
headers = response.each_header.to_h.with_indifferent_access
|
|
130
|
+
Download.new(
|
|
131
|
+
body: response.body,
|
|
132
|
+
filename: response_filename(response["Content-Disposition"]),
|
|
133
|
+
content_type: response["Content-Type"].to_s.split(";").first,
|
|
134
|
+
headers:
|
|
135
|
+
)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def response_filename(disposition)
|
|
139
|
+
encoded = disposition.to_s[/filename\*=(?:UTF-8'')?([^;]+)/i, 1]
|
|
140
|
+
return URI.decode_www_form_component(encoded.delete_prefix('"').delete_suffix('"')) if encoded
|
|
141
|
+
|
|
142
|
+
disposition.to_s[/filename="([^"]+)"/i, 1] ||
|
|
143
|
+
disposition.to_s[/filename=([^;]+)/i, 1]&.strip
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def encode_query(params, prefix = nil)
|
|
147
|
+
params.flat_map do |key, value|
|
|
148
|
+
full_key = prefix ? "#{prefix}[#{key}]" : key.to_s
|
|
149
|
+
if value.is_a?(Hash)
|
|
150
|
+
encode_query(value, full_key)
|
|
151
|
+
elsif value.is_a?(Array)
|
|
152
|
+
value.map { |item| encoded_pair("#{full_key}[]", item) }
|
|
153
|
+
else
|
|
154
|
+
encoded_pair(full_key, value)
|
|
155
|
+
end
|
|
156
|
+
end.join("&")
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def encoded_pair(key, value)
|
|
160
|
+
encoded_value = case value
|
|
161
|
+
when Time, Date then value.iso8601
|
|
162
|
+
else value.to_s
|
|
163
|
+
end
|
|
164
|
+
"#{URI.encode_www_form_component(key)}=#{URI.encode_www_form_component(encoded_value)}"
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
module Buezli
|
|
2
|
+
module Api
|
|
3
|
+
class Schema
|
|
4
|
+
attr_reader :version, :config
|
|
5
|
+
|
|
6
|
+
def self.load(path)
|
|
7
|
+
new(JSON.parse(Pathname(path).read))
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def initialize(config)
|
|
11
|
+
@config = config.with_indifferent_access
|
|
12
|
+
@version = @config.fetch(:version)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def endpoints
|
|
16
|
+
config.fetch(:endpoints, {})
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def for(model_name)
|
|
20
|
+
config.dig(:models, model_name.to_s)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def model_name_for_resource(resource_name)
|
|
24
|
+
config.fetch(:models).find do |_model_name, model|
|
|
25
|
+
model[:resource_name] == resource_name.to_s
|
|
26
|
+
end&.first
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def declared_field?(model_name, field_name)
|
|
30
|
+
model = self.for(model_name)
|
|
31
|
+
model&.fetch(:attributes, {})&.key?(field_name.to_s) ||
|
|
32
|
+
model&.fetch(:associations, {})&.key?(field_name.to_s)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def attribute_type(model_name, attribute)
|
|
36
|
+
self.for(model_name)&.dig(:attributes, attribute.to_s)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def association?(model_name, association)
|
|
40
|
+
self.for(model_name)&.fetch(:associations, {})&.key?(association.to_s)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def association_metadata(model_name, association)
|
|
44
|
+
self.for(model_name)&.dig(:associations, association.to_s) || {}.with_indifferent_access
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def association_class_name(model_name, association)
|
|
48
|
+
association_metadata(model_name, association)[:class_name] ||
|
|
49
|
+
association.to_s.singularize.classify
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def association_collection?(model_name, association)
|
|
53
|
+
association_metadata(model_name, association)[:type].to_s.include?("many")
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
data/lib/buezli/api.rb
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
require "active_support"
|
|
2
|
+
require "active_support/core_ext/hash/indifferent_access"
|
|
3
|
+
require "active_support/core_ext/object/blank"
|
|
4
|
+
require "active_support/core_ext/string/inflections"
|
|
5
|
+
require "date"
|
|
6
|
+
require "json"
|
|
7
|
+
require "net/http"
|
|
8
|
+
require "pathname"
|
|
9
|
+
require "time"
|
|
10
|
+
require "uri"
|
|
11
|
+
|
|
12
|
+
require_relative "api/version"
|
|
13
|
+
require_relative "api/schema"
|
|
14
|
+
require_relative "api/client/download"
|
|
15
|
+
require_relative "api/client/error"
|
|
16
|
+
require_relative "api/client/list"
|
|
17
|
+
require_relative "api/client/record"
|
|
18
|
+
require_relative "api/client"
|
|
@@ -0,0 +1,563 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "2026-07-14",
|
|
3
|
+
"models": {
|
|
4
|
+
"Client": {
|
|
5
|
+
"resource_name": "clients",
|
|
6
|
+
"attributes": {
|
|
7
|
+
"id": "integer",
|
|
8
|
+
"created_at": "datetime",
|
|
9
|
+
"updated_at": "datetime",
|
|
10
|
+
"name": "string",
|
|
11
|
+
"address": "string",
|
|
12
|
+
"currency": "string",
|
|
13
|
+
"locale": "string",
|
|
14
|
+
"archived_at": "datetime"
|
|
15
|
+
},
|
|
16
|
+
"associations": {}
|
|
17
|
+
},
|
|
18
|
+
"Contact": {
|
|
19
|
+
"resource_name": "contacts",
|
|
20
|
+
"attributes": {
|
|
21
|
+
"id": "integer",
|
|
22
|
+
"created_at": "datetime",
|
|
23
|
+
"updated_at": "datetime",
|
|
24
|
+
"client_id": "integer",
|
|
25
|
+
"first_name": "string",
|
|
26
|
+
"last_name": "string",
|
|
27
|
+
"title": "string",
|
|
28
|
+
"email": "string",
|
|
29
|
+
"phone_office": "string",
|
|
30
|
+
"phone_mobile": "string",
|
|
31
|
+
"archived_at": "datetime"
|
|
32
|
+
},
|
|
33
|
+
"associations": {
|
|
34
|
+
"client": {
|
|
35
|
+
"type": "belongs_to",
|
|
36
|
+
"class_name": "Client",
|
|
37
|
+
"inline": false
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
"Estimate": {
|
|
42
|
+
"resource_name": "estimates",
|
|
43
|
+
"attributes": {
|
|
44
|
+
"id": "integer",
|
|
45
|
+
"created_at": "datetime",
|
|
46
|
+
"updated_at": "datetime",
|
|
47
|
+
"client_id": "integer",
|
|
48
|
+
"number": "string",
|
|
49
|
+
"status": "string",
|
|
50
|
+
"issue_date": "date",
|
|
51
|
+
"subject": "string",
|
|
52
|
+
"notes": "string",
|
|
53
|
+
"currency": "string",
|
|
54
|
+
"locale": "string",
|
|
55
|
+
"recipient_name": "string",
|
|
56
|
+
"recipient_address": "string",
|
|
57
|
+
"sender_name": "string",
|
|
58
|
+
"sender_address": "string",
|
|
59
|
+
"sender_email": "string",
|
|
60
|
+
"sender_phone": "string",
|
|
61
|
+
"sender_vat_number": "string",
|
|
62
|
+
"discount_rate": "decimal",
|
|
63
|
+
"tax_rate": "decimal",
|
|
64
|
+
"subtotal": "money",
|
|
65
|
+
"discount": "money",
|
|
66
|
+
"tax": "money",
|
|
67
|
+
"tax_breakdown": "array",
|
|
68
|
+
"total": "money",
|
|
69
|
+
"sent_at": "datetime",
|
|
70
|
+
"accepted_at": "datetime",
|
|
71
|
+
"declined_at": "datetime",
|
|
72
|
+
"archived_at": "datetime",
|
|
73
|
+
"pdf": "download"
|
|
74
|
+
},
|
|
75
|
+
"associations": {
|
|
76
|
+
"client": {
|
|
77
|
+
"type": "belongs_to",
|
|
78
|
+
"class_name": "Client",
|
|
79
|
+
"inline": false
|
|
80
|
+
},
|
|
81
|
+
"line_items": {
|
|
82
|
+
"type": "has_many",
|
|
83
|
+
"class_name": "EstimateLineItem",
|
|
84
|
+
"inline": true
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
"EstimateLineItem": {
|
|
89
|
+
"resource_name": "estimate_line_items",
|
|
90
|
+
"attributes": {
|
|
91
|
+
"id": "integer",
|
|
92
|
+
"created_at": "datetime",
|
|
93
|
+
"updated_at": "datetime",
|
|
94
|
+
"description": "string",
|
|
95
|
+
"quantity": "decimal",
|
|
96
|
+
"unit_price": "money",
|
|
97
|
+
"amount": "money",
|
|
98
|
+
"tax_rate_override": "decimal",
|
|
99
|
+
"effective_tax_rate": "decimal"
|
|
100
|
+
},
|
|
101
|
+
"associations": {}
|
|
102
|
+
},
|
|
103
|
+
"Expense": {
|
|
104
|
+
"resource_name": "expenses",
|
|
105
|
+
"attributes": {
|
|
106
|
+
"id": "integer",
|
|
107
|
+
"created_at": "datetime",
|
|
108
|
+
"updated_at": "datetime",
|
|
109
|
+
"client_id": "integer",
|
|
110
|
+
"project_id": "integer",
|
|
111
|
+
"expense_category_id": "integer",
|
|
112
|
+
"invoice_id": "integer",
|
|
113
|
+
"spent_on": "date",
|
|
114
|
+
"calculation_type": "string",
|
|
115
|
+
"quantity": "decimal",
|
|
116
|
+
"unit": "string",
|
|
117
|
+
"unit_rate": "money",
|
|
118
|
+
"total": "money",
|
|
119
|
+
"currency": "string",
|
|
120
|
+
"notes": "string",
|
|
121
|
+
"billable": "boolean",
|
|
122
|
+
"billed": "boolean",
|
|
123
|
+
"reimbursed": "boolean",
|
|
124
|
+
"archived_at": "datetime",
|
|
125
|
+
"receipt": "download"
|
|
126
|
+
},
|
|
127
|
+
"associations": {
|
|
128
|
+
"client": {
|
|
129
|
+
"type": "has_one",
|
|
130
|
+
"class_name": "Client",
|
|
131
|
+
"inline": false
|
|
132
|
+
},
|
|
133
|
+
"project": {
|
|
134
|
+
"type": "belongs_to",
|
|
135
|
+
"class_name": "Project",
|
|
136
|
+
"inline": false
|
|
137
|
+
},
|
|
138
|
+
"expense_category": {
|
|
139
|
+
"type": "belongs_to",
|
|
140
|
+
"class_name": "ExpenseCategory",
|
|
141
|
+
"inline": false
|
|
142
|
+
},
|
|
143
|
+
"invoice": {
|
|
144
|
+
"type": "belongs_to",
|
|
145
|
+
"class_name": "Invoice",
|
|
146
|
+
"inline": false
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
"ExpenseCategory": {
|
|
151
|
+
"resource_name": "expense_categories",
|
|
152
|
+
"attributes": {
|
|
153
|
+
"id": "integer",
|
|
154
|
+
"created_at": "datetime",
|
|
155
|
+
"updated_at": "datetime",
|
|
156
|
+
"name": "string",
|
|
157
|
+
"archived_at": "datetime"
|
|
158
|
+
},
|
|
159
|
+
"associations": {}
|
|
160
|
+
},
|
|
161
|
+
"Invoice": {
|
|
162
|
+
"resource_name": "invoices",
|
|
163
|
+
"attributes": {
|
|
164
|
+
"id": "integer",
|
|
165
|
+
"created_at": "datetime",
|
|
166
|
+
"updated_at": "datetime",
|
|
167
|
+
"client_id": "integer",
|
|
168
|
+
"source_estimate_id": "integer",
|
|
169
|
+
"number": "string",
|
|
170
|
+
"status": "string",
|
|
171
|
+
"issue_date": "date",
|
|
172
|
+
"due_date": "date",
|
|
173
|
+
"subject": "string",
|
|
174
|
+
"notes": "string",
|
|
175
|
+
"currency": "string",
|
|
176
|
+
"locale": "string",
|
|
177
|
+
"recipient_name": "string",
|
|
178
|
+
"recipient_address": "string",
|
|
179
|
+
"sender_name": "string",
|
|
180
|
+
"sender_address": "string",
|
|
181
|
+
"sender_email": "string",
|
|
182
|
+
"sender_phone": "string",
|
|
183
|
+
"sender_vat_number": "string",
|
|
184
|
+
"sender_iban": "string",
|
|
185
|
+
"payment_reference": "string",
|
|
186
|
+
"tax_rate": "decimal",
|
|
187
|
+
"discount_rate": "decimal",
|
|
188
|
+
"subtotal": "money",
|
|
189
|
+
"discount": "money",
|
|
190
|
+
"tax": "money",
|
|
191
|
+
"tax_breakdown": "array",
|
|
192
|
+
"total": "money",
|
|
193
|
+
"paid": "money",
|
|
194
|
+
"due": "money",
|
|
195
|
+
"sent_at": "datetime",
|
|
196
|
+
"paid_at": "datetime",
|
|
197
|
+
"voided_at": "datetime",
|
|
198
|
+
"void_reason": "string",
|
|
199
|
+
"archived_at": "datetime",
|
|
200
|
+
"recurring_invoice_template_id": "integer",
|
|
201
|
+
"recurring_occurrence_on": "date",
|
|
202
|
+
"pdf": "download"
|
|
203
|
+
},
|
|
204
|
+
"associations": {
|
|
205
|
+
"client": {
|
|
206
|
+
"type": "belongs_to",
|
|
207
|
+
"class_name": "Client",
|
|
208
|
+
"inline": false
|
|
209
|
+
},
|
|
210
|
+
"source_estimate": {
|
|
211
|
+
"type": "belongs_to",
|
|
212
|
+
"class_name": "Estimate",
|
|
213
|
+
"inline": false
|
|
214
|
+
},
|
|
215
|
+
"recurring_invoice_template": {
|
|
216
|
+
"type": "belongs_to",
|
|
217
|
+
"class_name": "RecurringInvoiceTemplate",
|
|
218
|
+
"inline": false
|
|
219
|
+
},
|
|
220
|
+
"line_items": {
|
|
221
|
+
"type": "has_many",
|
|
222
|
+
"class_name": "InvoiceLineItem",
|
|
223
|
+
"inline": true
|
|
224
|
+
},
|
|
225
|
+
"payments": {
|
|
226
|
+
"type": "has_many",
|
|
227
|
+
"class_name": "Payment",
|
|
228
|
+
"inline": true
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
},
|
|
232
|
+
"InvoiceLineItem": {
|
|
233
|
+
"resource_name": "invoice_line_items",
|
|
234
|
+
"attributes": {
|
|
235
|
+
"id": "integer",
|
|
236
|
+
"created_at": "datetime",
|
|
237
|
+
"updated_at": "datetime",
|
|
238
|
+
"description": "string",
|
|
239
|
+
"quantity": "decimal",
|
|
240
|
+
"unit_price": "money",
|
|
241
|
+
"amount": "money",
|
|
242
|
+
"tax_rate_override": "decimal",
|
|
243
|
+
"effective_tax_rate": "decimal",
|
|
244
|
+
"project_id": "integer",
|
|
245
|
+
"source_kind": "string"
|
|
246
|
+
},
|
|
247
|
+
"associations": {
|
|
248
|
+
"project": {
|
|
249
|
+
"type": "belongs_to",
|
|
250
|
+
"class_name": "Project",
|
|
251
|
+
"inline": false
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
},
|
|
255
|
+
"Payment": {
|
|
256
|
+
"resource_name": "payments",
|
|
257
|
+
"attributes": {
|
|
258
|
+
"id": "integer",
|
|
259
|
+
"created_at": "datetime",
|
|
260
|
+
"updated_at": "datetime",
|
|
261
|
+
"invoice_id": "integer",
|
|
262
|
+
"reversal_of_id": "integer",
|
|
263
|
+
"amount": "money",
|
|
264
|
+
"paid_on": "date",
|
|
265
|
+
"notes": "string"
|
|
266
|
+
},
|
|
267
|
+
"associations": {
|
|
268
|
+
"invoice": {
|
|
269
|
+
"type": "belongs_to",
|
|
270
|
+
"class_name": "Invoice",
|
|
271
|
+
"inline": false
|
|
272
|
+
},
|
|
273
|
+
"reversal_of": {
|
|
274
|
+
"type": "belongs_to",
|
|
275
|
+
"class_name": "Payment",
|
|
276
|
+
"inline": false
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
},
|
|
280
|
+
"Project": {
|
|
281
|
+
"resource_name": "projects",
|
|
282
|
+
"attributes": {
|
|
283
|
+
"id": "integer",
|
|
284
|
+
"created_at": "datetime",
|
|
285
|
+
"updated_at": "datetime",
|
|
286
|
+
"client_id": "integer",
|
|
287
|
+
"name": "string",
|
|
288
|
+
"notes": "string",
|
|
289
|
+
"currency": "string",
|
|
290
|
+
"project_type": "string",
|
|
291
|
+
"starts_on": "date",
|
|
292
|
+
"ends_on": "date",
|
|
293
|
+
"archived_at": "datetime"
|
|
294
|
+
},
|
|
295
|
+
"associations": {
|
|
296
|
+
"client": {
|
|
297
|
+
"type": "belongs_to",
|
|
298
|
+
"class_name": "Client",
|
|
299
|
+
"inline": false
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
},
|
|
303
|
+
"RecurringInvoiceLineItem": {
|
|
304
|
+
"resource_name": "recurring_invoice_line_items",
|
|
305
|
+
"attributes": {
|
|
306
|
+
"id": "integer",
|
|
307
|
+
"created_at": "datetime",
|
|
308
|
+
"updated_at": "datetime",
|
|
309
|
+
"description": "string",
|
|
310
|
+
"quantity": "decimal",
|
|
311
|
+
"unit_price": "money",
|
|
312
|
+
"amount": "money",
|
|
313
|
+
"tax_rate_override": "decimal",
|
|
314
|
+
"effective_tax_rate": "decimal"
|
|
315
|
+
},
|
|
316
|
+
"associations": {}
|
|
317
|
+
},
|
|
318
|
+
"RecurringInvoiceTemplate": {
|
|
319
|
+
"resource_name": "recurring_invoice_templates",
|
|
320
|
+
"attributes": {
|
|
321
|
+
"id": "integer",
|
|
322
|
+
"created_at": "datetime",
|
|
323
|
+
"updated_at": "datetime",
|
|
324
|
+
"client_id": "integer",
|
|
325
|
+
"name": "string",
|
|
326
|
+
"currency": "string",
|
|
327
|
+
"locale": "string",
|
|
328
|
+
"subject": "string",
|
|
329
|
+
"notes": "string",
|
|
330
|
+
"discount_rate": "decimal",
|
|
331
|
+
"tax_rate": "decimal",
|
|
332
|
+
"interval_unit": "string",
|
|
333
|
+
"interval_count": "integer",
|
|
334
|
+
"starts_on": "date",
|
|
335
|
+
"next_issue_on": "date",
|
|
336
|
+
"ends_on": "date",
|
|
337
|
+
"due_days": "integer",
|
|
338
|
+
"paused_at": "datetime",
|
|
339
|
+
"archived_at": "datetime"
|
|
340
|
+
},
|
|
341
|
+
"associations": {
|
|
342
|
+
"client": {
|
|
343
|
+
"type": "belongs_to",
|
|
344
|
+
"class_name": "Client",
|
|
345
|
+
"inline": false
|
|
346
|
+
},
|
|
347
|
+
"line_items": {
|
|
348
|
+
"type": "has_many",
|
|
349
|
+
"class_name": "RecurringInvoiceLineItem",
|
|
350
|
+
"inline": true
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
},
|
|
354
|
+
"Task": {
|
|
355
|
+
"resource_name": "tasks",
|
|
356
|
+
"attributes": {
|
|
357
|
+
"id": "integer",
|
|
358
|
+
"created_at": "datetime",
|
|
359
|
+
"updated_at": "datetime",
|
|
360
|
+
"name": "string",
|
|
361
|
+
"archived_at": "datetime"
|
|
362
|
+
},
|
|
363
|
+
"associations": {}
|
|
364
|
+
},
|
|
365
|
+
"TimeEntry": {
|
|
366
|
+
"resource_name": "time_entries",
|
|
367
|
+
"attributes": {
|
|
368
|
+
"id": "integer",
|
|
369
|
+
"created_at": "datetime",
|
|
370
|
+
"updated_at": "datetime",
|
|
371
|
+
"client_id": "integer",
|
|
372
|
+
"project_id": "integer",
|
|
373
|
+
"task_id": "integer",
|
|
374
|
+
"invoice_id": "integer",
|
|
375
|
+
"spent_on": "date",
|
|
376
|
+
"duration_seconds": "integer",
|
|
377
|
+
"notes": "string",
|
|
378
|
+
"billable": "boolean",
|
|
379
|
+
"billed": "boolean",
|
|
380
|
+
"billable_rate": "money",
|
|
381
|
+
"cost_rate": "money"
|
|
382
|
+
},
|
|
383
|
+
"associations": {
|
|
384
|
+
"client": {
|
|
385
|
+
"type": "has_one",
|
|
386
|
+
"class_name": "Client",
|
|
387
|
+
"inline": false
|
|
388
|
+
},
|
|
389
|
+
"project": {
|
|
390
|
+
"type": "belongs_to",
|
|
391
|
+
"class_name": "Project",
|
|
392
|
+
"inline": false
|
|
393
|
+
},
|
|
394
|
+
"task": {
|
|
395
|
+
"type": "belongs_to",
|
|
396
|
+
"class_name": "Task",
|
|
397
|
+
"inline": false
|
|
398
|
+
},
|
|
399
|
+
"invoice": {
|
|
400
|
+
"type": "belongs_to",
|
|
401
|
+
"class_name": "Invoice",
|
|
402
|
+
"inline": false
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
},
|
|
407
|
+
"endpoints": {
|
|
408
|
+
"clients": [
|
|
409
|
+
{
|
|
410
|
+
"action": "index",
|
|
411
|
+
"verb": "GET",
|
|
412
|
+
"path": "/2026-07-14/clients"
|
|
413
|
+
},
|
|
414
|
+
{
|
|
415
|
+
"action": "show",
|
|
416
|
+
"verb": "GET",
|
|
417
|
+
"path": "/2026-07-14/clients/:id"
|
|
418
|
+
}
|
|
419
|
+
],
|
|
420
|
+
"contacts": [
|
|
421
|
+
{
|
|
422
|
+
"action": "index",
|
|
423
|
+
"verb": "GET",
|
|
424
|
+
"path": "/2026-07-14/contacts"
|
|
425
|
+
},
|
|
426
|
+
{
|
|
427
|
+
"action": "show",
|
|
428
|
+
"verb": "GET",
|
|
429
|
+
"path": "/2026-07-14/contacts/:id"
|
|
430
|
+
}
|
|
431
|
+
],
|
|
432
|
+
"estimates": [
|
|
433
|
+
{
|
|
434
|
+
"action": "pdf",
|
|
435
|
+
"verb": "GET",
|
|
436
|
+
"path": "/2026-07-14/estimates/:id/pdf"
|
|
437
|
+
},
|
|
438
|
+
{
|
|
439
|
+
"action": "index",
|
|
440
|
+
"verb": "GET",
|
|
441
|
+
"path": "/2026-07-14/estimates"
|
|
442
|
+
},
|
|
443
|
+
{
|
|
444
|
+
"action": "show",
|
|
445
|
+
"verb": "GET",
|
|
446
|
+
"path": "/2026-07-14/estimates/:id"
|
|
447
|
+
}
|
|
448
|
+
],
|
|
449
|
+
"expense_categories": [
|
|
450
|
+
{
|
|
451
|
+
"action": "index",
|
|
452
|
+
"verb": "GET",
|
|
453
|
+
"path": "/2026-07-14/expense_categories"
|
|
454
|
+
},
|
|
455
|
+
{
|
|
456
|
+
"action": "show",
|
|
457
|
+
"verb": "GET",
|
|
458
|
+
"path": "/2026-07-14/expense_categories/:id"
|
|
459
|
+
}
|
|
460
|
+
],
|
|
461
|
+
"expenses": [
|
|
462
|
+
{
|
|
463
|
+
"action": "receipt",
|
|
464
|
+
"verb": "GET",
|
|
465
|
+
"path": "/2026-07-14/expenses/:id/receipt"
|
|
466
|
+
},
|
|
467
|
+
{
|
|
468
|
+
"action": "index",
|
|
469
|
+
"verb": "GET",
|
|
470
|
+
"path": "/2026-07-14/expenses"
|
|
471
|
+
},
|
|
472
|
+
{
|
|
473
|
+
"action": "show",
|
|
474
|
+
"verb": "GET",
|
|
475
|
+
"path": "/2026-07-14/expenses/:id"
|
|
476
|
+
}
|
|
477
|
+
],
|
|
478
|
+
"invoices": [
|
|
479
|
+
{
|
|
480
|
+
"action": "pdf",
|
|
481
|
+
"verb": "GET",
|
|
482
|
+
"path": "/2026-07-14/invoices/:id/pdf"
|
|
483
|
+
},
|
|
484
|
+
{
|
|
485
|
+
"action": "index",
|
|
486
|
+
"verb": "GET",
|
|
487
|
+
"path": "/2026-07-14/invoices"
|
|
488
|
+
},
|
|
489
|
+
{
|
|
490
|
+
"action": "show",
|
|
491
|
+
"verb": "GET",
|
|
492
|
+
"path": "/2026-07-14/invoices/:id"
|
|
493
|
+
}
|
|
494
|
+
],
|
|
495
|
+
"payments": [
|
|
496
|
+
{
|
|
497
|
+
"action": "index",
|
|
498
|
+
"verb": "GET",
|
|
499
|
+
"path": "/2026-07-14/payments"
|
|
500
|
+
},
|
|
501
|
+
{
|
|
502
|
+
"action": "show",
|
|
503
|
+
"verb": "GET",
|
|
504
|
+
"path": "/2026-07-14/payments/:id"
|
|
505
|
+
}
|
|
506
|
+
],
|
|
507
|
+
"projects": [
|
|
508
|
+
{
|
|
509
|
+
"action": "index",
|
|
510
|
+
"verb": "GET",
|
|
511
|
+
"path": "/2026-07-14/projects"
|
|
512
|
+
},
|
|
513
|
+
{
|
|
514
|
+
"action": "show",
|
|
515
|
+
"verb": "GET",
|
|
516
|
+
"path": "/2026-07-14/projects/:id"
|
|
517
|
+
}
|
|
518
|
+
],
|
|
519
|
+
"recurring_invoice_templates": [
|
|
520
|
+
{
|
|
521
|
+
"action": "index",
|
|
522
|
+
"verb": "GET",
|
|
523
|
+
"path": "/2026-07-14/recurring_invoice_templates"
|
|
524
|
+
},
|
|
525
|
+
{
|
|
526
|
+
"action": "show",
|
|
527
|
+
"verb": "GET",
|
|
528
|
+
"path": "/2026-07-14/recurring_invoice_templates/:id"
|
|
529
|
+
}
|
|
530
|
+
],
|
|
531
|
+
"schema": [
|
|
532
|
+
{
|
|
533
|
+
"action": "show",
|
|
534
|
+
"verb": "GET",
|
|
535
|
+
"path": "/2026-07-14/schema"
|
|
536
|
+
}
|
|
537
|
+
],
|
|
538
|
+
"tasks": [
|
|
539
|
+
{
|
|
540
|
+
"action": "index",
|
|
541
|
+
"verb": "GET",
|
|
542
|
+
"path": "/2026-07-14/tasks"
|
|
543
|
+
},
|
|
544
|
+
{
|
|
545
|
+
"action": "show",
|
|
546
|
+
"verb": "GET",
|
|
547
|
+
"path": "/2026-07-14/tasks/:id"
|
|
548
|
+
}
|
|
549
|
+
],
|
|
550
|
+
"time_entries": [
|
|
551
|
+
{
|
|
552
|
+
"action": "index",
|
|
553
|
+
"verb": "GET",
|
|
554
|
+
"path": "/2026-07-14/time_entries"
|
|
555
|
+
},
|
|
556
|
+
{
|
|
557
|
+
"action": "show",
|
|
558
|
+
"verb": "GET",
|
|
559
|
+
"path": "/2026-07-14/time_entries/:id"
|
|
560
|
+
}
|
|
561
|
+
]
|
|
562
|
+
}
|
|
563
|
+
}
|
metadata
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: buezli-api
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Codegestalt
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: activesupport
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '7.2'
|
|
19
|
+
- - "<"
|
|
20
|
+
- !ruby/object:Gem::Version
|
|
21
|
+
version: '9'
|
|
22
|
+
type: :runtime
|
|
23
|
+
prerelease: false
|
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
25
|
+
requirements:
|
|
26
|
+
- - ">="
|
|
27
|
+
- !ruby/object:Gem::Version
|
|
28
|
+
version: '7.2'
|
|
29
|
+
- - "<"
|
|
30
|
+
- !ruby/object:Gem::Version
|
|
31
|
+
version: '9'
|
|
32
|
+
- !ruby/object:Gem::Dependency
|
|
33
|
+
name: minitest
|
|
34
|
+
requirement: !ruby/object:Gem::Requirement
|
|
35
|
+
requirements:
|
|
36
|
+
- - "~>"
|
|
37
|
+
- !ruby/object:Gem::Version
|
|
38
|
+
version: '5.0'
|
|
39
|
+
type: :development
|
|
40
|
+
prerelease: false
|
|
41
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
42
|
+
requirements:
|
|
43
|
+
- - "~>"
|
|
44
|
+
- !ruby/object:Gem::Version
|
|
45
|
+
version: '5.0'
|
|
46
|
+
- !ruby/object:Gem::Dependency
|
|
47
|
+
name: rake
|
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
|
49
|
+
requirements:
|
|
50
|
+
- - "~>"
|
|
51
|
+
- !ruby/object:Gem::Version
|
|
52
|
+
version: '13.0'
|
|
53
|
+
type: :development
|
|
54
|
+
prerelease: false
|
|
55
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
56
|
+
requirements:
|
|
57
|
+
- - "~>"
|
|
58
|
+
- !ruby/object:Gem::Version
|
|
59
|
+
version: '13.0'
|
|
60
|
+
description: A read-only Ruby client generated from Buezli's versioned API schema.
|
|
61
|
+
executables: []
|
|
62
|
+
extensions: []
|
|
63
|
+
extra_rdoc_files: []
|
|
64
|
+
files:
|
|
65
|
+
- LICENSE
|
|
66
|
+
- README.md
|
|
67
|
+
- lib/buezli/api.rb
|
|
68
|
+
- lib/buezli/api/client.rb
|
|
69
|
+
- lib/buezli/api/client/download.rb
|
|
70
|
+
- lib/buezli/api/client/error.rb
|
|
71
|
+
- lib/buezli/api/client/list.rb
|
|
72
|
+
- lib/buezli/api/client/record.rb
|
|
73
|
+
- lib/buezli/api/schema.rb
|
|
74
|
+
- lib/buezli/api/version.rb
|
|
75
|
+
- schemas/2026-07-14.json
|
|
76
|
+
homepage: https://buezli.app/api
|
|
77
|
+
licenses:
|
|
78
|
+
- MIT
|
|
79
|
+
metadata:
|
|
80
|
+
source_code_uri: https://github.com/codegestalt/buezli-api
|
|
81
|
+
documentation_uri: https://buezli.app/api
|
|
82
|
+
bug_tracker_uri: https://github.com/codegestalt/buezli-api/issues
|
|
83
|
+
rubygems_mfa_required: 'true'
|
|
84
|
+
rdoc_options: []
|
|
85
|
+
require_paths:
|
|
86
|
+
- lib
|
|
87
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
88
|
+
requirements:
|
|
89
|
+
- - ">="
|
|
90
|
+
- !ruby/object:Gem::Version
|
|
91
|
+
version: '3.2'
|
|
92
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
93
|
+
requirements:
|
|
94
|
+
- - ">="
|
|
95
|
+
- !ruby/object:Gem::Version
|
|
96
|
+
version: '0'
|
|
97
|
+
requirements: []
|
|
98
|
+
rubygems_version: 4.0.17
|
|
99
|
+
specification_version: 4
|
|
100
|
+
summary: Schema-driven Ruby client for the Buezli API
|
|
101
|
+
test_files: []
|