keinaufwand 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/README.md +28 -0
- data/lib/keinaufwand/api/client/error.rb +24 -0
- data/lib/keinaufwand/api/client/list.rb +52 -0
- data/lib/keinaufwand/api/client/record.rb +81 -0
- data/lib/keinaufwand/api/client.rb +119 -0
- data/lib/keinaufwand/api/configuration.rb +23 -0
- data/lib/keinaufwand/api/schema.rb +55 -0
- data/lib/keinaufwand/api/version.rb +5 -0
- data/lib/keinaufwand/api.rb +38 -0
- data/lib/keinaufwand.rb +1 -0
- data/schemas/2026-07-02.json +400 -0
- metadata +67 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 1e1378e93d8b47d1745d12b04a20b9022cbc097506e02ab0ecfc13f1c5ef6c41
|
|
4
|
+
data.tar.gz: 55d31421f07efea09ed4ee2f766cb73ae3f37d038a50aec63135de8c705cb8e8
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 459887568f842948dfc5200138393efb8ba9b07aac95eb0b0105f0b2e547c49854686101cde0c30774d960f6e7db6c0ca8f5cd3bb06aa3acdbfaad138ce7e738
|
|
7
|
+
data.tar.gz: 4da77f43b1d6f10f4837b0a96302f67357121c169639b8b5de1d668de158dc89bd4573b415dc9139cbc3597f2d2b9383de205bb81bfee7c6986d0348cd08f359
|
data/README.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# keinaufwand
|
|
2
|
+
|
|
3
|
+
Ruby client for the [Keinaufwand API](https://keinaufwand.com). Includes the API schema and typed nested records.
|
|
4
|
+
|
|
5
|
+
```ruby
|
|
6
|
+
gem "keinaufwand", "~> 0.1.0"
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
require "keinaufwand"
|
|
11
|
+
|
|
12
|
+
client = Keinaufwand::Api::Client.new(
|
|
13
|
+
access_token: ENV.fetch("KEINAUFWAND_API_KEY"),
|
|
14
|
+
version: "2026-07-02"
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
page = client.retrieve_posts(limit: 100)
|
|
18
|
+
while page
|
|
19
|
+
page.each { |post| puts post.title }
|
|
20
|
+
page = page.next
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
post = client.retrieve_post(123)
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Requests require an account API key. Collection methods return an enumerable page with pagination metadata; show methods return one record. HTTP failures raise `Keinaufwand::Api::Client::Error`.
|
|
27
|
+
|
|
28
|
+
For Rails persistence, local media, delta polling, and signed webhooks, install `keinaufwand-sync` as well.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
module Keinaufwand
|
|
2
|
+
module Api
|
|
3
|
+
class Client
|
|
4
|
+
class Error < StandardError
|
|
5
|
+
attr_reader :response
|
|
6
|
+
|
|
7
|
+
def initialize(response)
|
|
8
|
+
@response = response
|
|
9
|
+
return super(response) if response.is_a?(String)
|
|
10
|
+
|
|
11
|
+
message = response.body.presence || "#{response.code} #{response.message}"
|
|
12
|
+
|
|
13
|
+
begin
|
|
14
|
+
parsed = JSON.parse(response.body)
|
|
15
|
+
message = parsed.dig("error", "message") || message
|
|
16
|
+
rescue JSON::ParserError, TypeError
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
super(message)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
module Keinaufwand
|
|
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 || {}
|
|
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 count size
|
|
29
|
+
alias length size
|
|
30
|
+
|
|
31
|
+
def last
|
|
32
|
+
data.last
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def [](index)
|
|
36
|
+
data[index]
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def next
|
|
40
|
+
next_page = meta.dig("pagination", "next")
|
|
41
|
+
return unless next_page
|
|
42
|
+
|
|
43
|
+
current_page = @request_context.dig(:params, :page)
|
|
44
|
+
raise Error, "Infinite pagination loop detected" if current_page == next_page
|
|
45
|
+
|
|
46
|
+
params = @request_context[:params].merge(page: next_page)
|
|
47
|
+
@client.send(:perform_request, @request_context[:verb], @request_context[:path], params, model_name: @request_context[:model_name])
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
module Keinaufwand
|
|
2
|
+
module Api
|
|
3
|
+
class Client
|
|
4
|
+
class Record
|
|
5
|
+
attr_reader :raw_attributes, :model_name, :client, :schema, :server_updated_at
|
|
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
|
+
@server_updated_at = parse_time(@raw_attributes[:updated_at])
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def id
|
|
16
|
+
raw_attributes[:id]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def [](key)
|
|
20
|
+
raw_attributes[key]
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def []=(key, value)
|
|
24
|
+
raw_attributes[key] = value
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def method_missing(method, *args, &block)
|
|
28
|
+
method_name = method.to_s
|
|
29
|
+
|
|
30
|
+
if raw_attributes.key?(method_name)
|
|
31
|
+
return read_value(method_name)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
return nil if args.empty? && !method_name.end_with?("=")
|
|
35
|
+
|
|
36
|
+
super
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def respond_to_missing?(method, include_private = false)
|
|
40
|
+
raw_attributes.key?(method.to_s) || super
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def read_value(name)
|
|
46
|
+
value = raw_attributes[name]
|
|
47
|
+
type = schema&.dig("attributes", name)
|
|
48
|
+
|
|
49
|
+
if client.schema.association?(model_name, name)
|
|
50
|
+
return build_association(name, value)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
case type.to_s
|
|
54
|
+
when "datetime" then parse_time(value) || value
|
|
55
|
+
when "date" then Date.parse(value.to_s) rescue value
|
|
56
|
+
when "amount" then value
|
|
57
|
+
else value
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def build_association(name, value)
|
|
62
|
+
return if value.nil?
|
|
63
|
+
|
|
64
|
+
target_model = client.schema.association_class_name(model_name, name)
|
|
65
|
+
|
|
66
|
+
if client.schema.association_collection?(model_name, name)
|
|
67
|
+
Array(value).map { |attributes| client.build_record(target_model, attributes) }
|
|
68
|
+
else
|
|
69
|
+
client.build_record(target_model, value)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def parse_time(value)
|
|
74
|
+
Time.parse(value.to_s) if value.present?
|
|
75
|
+
rescue ArgumentError
|
|
76
|
+
nil
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
module Keinaufwand
|
|
2
|
+
module Api
|
|
3
|
+
class Client
|
|
4
|
+
attr_reader :access_token, :endpoint, :version, :schema, :host
|
|
5
|
+
|
|
6
|
+
def initialize(access_token:, endpoint: "https://api.keinaufwand.com", version: nil, schema: nil, host: nil)
|
|
7
|
+
@access_token = access_token
|
|
8
|
+
@endpoint = endpoint.chomp("/")
|
|
9
|
+
@version = version || Keinaufwand::Api.configuration.version || "2026-07-02"
|
|
10
|
+
@host = host
|
|
11
|
+
@schema = schema || load_schema
|
|
12
|
+
define_methods_from_schema
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def build_record(model_name, attributes)
|
|
16
|
+
Record.new(
|
|
17
|
+
attributes: attributes || {},
|
|
18
|
+
model_name: model_name,
|
|
19
|
+
client: self,
|
|
20
|
+
schema: schema.for(model_name)
|
|
21
|
+
)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def load_schema
|
|
27
|
+
Schema.load(Keinaufwand::Api.configuration.schema_path(version))
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def define_methods_from_schema
|
|
31
|
+
schema.endpoints.each do |resource, endpoints|
|
|
32
|
+
endpoints.each do |endpoint_config|
|
|
33
|
+
path = endpoint_config.fetch("path")
|
|
34
|
+
verb = endpoint_config.fetch("verb")
|
|
35
|
+
model_name = resource.singularize.classify
|
|
36
|
+
method_name = method_name_for(endpoint_config.fetch("action"), resource)
|
|
37
|
+
|
|
38
|
+
define_singleton_method(method_name) do |*args|
|
|
39
|
+
params = args.last.is_a?(Hash) ? args.pop : {}
|
|
40
|
+
dynamic_path = path.dup
|
|
41
|
+
dynamic_path.scan(/:(\w+)/).flatten.each do |key|
|
|
42
|
+
value = params.delete(key.to_sym) || params.delete(key) || args.shift
|
|
43
|
+
raise ArgumentError, "Missing required path parameter: #{key}" if value.nil?
|
|
44
|
+
|
|
45
|
+
dynamic_path = dynamic_path.gsub(":#{key}", value.to_s)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
perform_request(verb, dynamic_path, params, model_name: model_name)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def method_name_for(action, resource)
|
|
55
|
+
prefix = action == "show" ? "retrieve" : action == "index" ? "retrieve" : action
|
|
56
|
+
noun = action == "index" ? resource.pluralize : resource.singularize
|
|
57
|
+
"#{prefix}_#{noun}".to_sym
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def perform_request(verb, path, params = {}, model_name:)
|
|
61
|
+
payload = perform_json(verb, path, params)
|
|
62
|
+
data = payload.is_a?(Hash) && payload.key?("data") ? payload["data"] : payload
|
|
63
|
+
|
|
64
|
+
records = if data.is_a?(Array)
|
|
65
|
+
data.map { |attributes| build_record(model_name, attributes) }
|
|
66
|
+
elsif data.is_a?(Hash)
|
|
67
|
+
build_record(model_name, data)
|
|
68
|
+
else
|
|
69
|
+
data
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
return records unless records.is_a?(Array)
|
|
73
|
+
|
|
74
|
+
List.new(
|
|
75
|
+
data: records,
|
|
76
|
+
meta: payload["meta"],
|
|
77
|
+
client: self,
|
|
78
|
+
request_context: { verb: verb, path: path, params: params, model_name: model_name }
|
|
79
|
+
)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def perform_json(verb, path, params)
|
|
83
|
+
uri = URI("#{endpoint}#{path}")
|
|
84
|
+
if verb == "GET" && params.present?
|
|
85
|
+
uri.query = encode_query(params)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
request = request_for(verb, uri)
|
|
89
|
+
request["Authorization"] = "Token #{access_token}"
|
|
90
|
+
request["Accept"] = "application/json"
|
|
91
|
+
request["Content-Type"] = "application/json"
|
|
92
|
+
request["Host"] = host if host
|
|
93
|
+
request.body = JSON.generate(params) if verb != "GET" && params.present?
|
|
94
|
+
|
|
95
|
+
response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") { |http| http.request(request) }
|
|
96
|
+
raise Error.new(response) unless response.is_a?(Net::HTTPSuccess)
|
|
97
|
+
|
|
98
|
+
JSON.parse(response.body)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def request_for(verb, uri)
|
|
102
|
+
Net::HTTP.const_get(verb.to_s.capitalize).new(uri)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def encode_query(params, prefix = nil)
|
|
106
|
+
params.flat_map do |key, value|
|
|
107
|
+
full_key = prefix ? "#{prefix}[#{key}]" : key.to_s
|
|
108
|
+
if value.is_a?(Hash)
|
|
109
|
+
encode_query(value, full_key)
|
|
110
|
+
elsif value.is_a?(Array)
|
|
111
|
+
value.map { |item| "#{URI.encode_www_form_component(full_key)}[]=#{URI.encode_www_form_component(item.to_s)}" }
|
|
112
|
+
else
|
|
113
|
+
"#{URI.encode_www_form_component(full_key)}=#{URI.encode_www_form_component(value.to_s)}"
|
|
114
|
+
end
|
|
115
|
+
end.join("&")
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module Keinaufwand
|
|
2
|
+
module Api
|
|
3
|
+
class Configuration
|
|
4
|
+
attr_accessor :path
|
|
5
|
+
|
|
6
|
+
def initialize(path = nil)
|
|
7
|
+
@path = path
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def values
|
|
11
|
+
@values ||= path && File.exist?(path) ? YAML.load_file(path).with_indifferent_access : {}.with_indifferent_access
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def version
|
|
15
|
+
values.dig(:api, :version) || values[:version]
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def schema_path(version = self.version)
|
|
19
|
+
File.expand_path("../../../schemas/#{version}.json", __dir__)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
module Keinaufwand
|
|
2
|
+
module Api
|
|
3
|
+
class Schema
|
|
4
|
+
attr_reader :version, :config
|
|
5
|
+
|
|
6
|
+
def self.load(path)
|
|
7
|
+
new(YAML.load_file(path))
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def initialize(config)
|
|
11
|
+
@config = config.with_indifferent_access
|
|
12
|
+
@version = @config.fetch(:version)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def for(model_name)
|
|
16
|
+
config.dig(:models, model_name.to_s)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def model_names
|
|
20
|
+
config.fetch(:models).keys
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def endpoints
|
|
24
|
+
config[:endpoints] || {}
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def association?(model_name, attribute)
|
|
28
|
+
association_type?(attribute_type(model_name, attribute)) || association_metadata(model_name, attribute).present?
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def association_type?(type)
|
|
32
|
+
%w[association has_one has_many belongs_to :has_one :has_many :belongs_to].include?(type.to_s)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def attribute_type(model_name, attribute)
|
|
36
|
+
model = self.for(model_name)
|
|
37
|
+
model&.dig(:attributes, attribute.to_s)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def association_metadata(model_name, attribute)
|
|
41
|
+
model = self.for(model_name)
|
|
42
|
+
model&.dig(:associations, attribute.to_s) || {}
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def association_class_name(model_name, attribute)
|
|
46
|
+
association_metadata(model_name, attribute)[:class_name] || attribute.to_s.singularize.classify
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def association_collection?(model_name, attribute)
|
|
50
|
+
type = association_metadata(model_name, attribute)[:type] || attribute_type(model_name, attribute)
|
|
51
|
+
type.to_s.include?("many")
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
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/inflector"
|
|
5
|
+
require "json"
|
|
6
|
+
require "net/http"
|
|
7
|
+
require "pathname"
|
|
8
|
+
require "time"
|
|
9
|
+
require "uri"
|
|
10
|
+
require "yaml"
|
|
11
|
+
|
|
12
|
+
require_relative "api/version"
|
|
13
|
+
require_relative "api/configuration"
|
|
14
|
+
require_relative "api/schema"
|
|
15
|
+
require_relative "api/client/error"
|
|
16
|
+
require_relative "api/client/list"
|
|
17
|
+
require_relative "api/client/record"
|
|
18
|
+
require_relative "api/client"
|
|
19
|
+
|
|
20
|
+
module Keinaufwand
|
|
21
|
+
module Api
|
|
22
|
+
class << self
|
|
23
|
+
def configuration
|
|
24
|
+
@configuration ||= Configuration.new(default_config_path)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def configure
|
|
28
|
+
yield configuration
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def default_config_path
|
|
34
|
+
Rails.root.join("config/keinaufwand.yml") if defined?(Rails) && Rails.respond_to?(:root) && Rails.root
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
data/lib/keinaufwand.rb
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
require_relative "keinaufwand/api"
|
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "2026-07-02",
|
|
3
|
+
"models": {
|
|
4
|
+
"Address": {
|
|
5
|
+
"resource_name": "addresses",
|
|
6
|
+
"attributes": {
|
|
7
|
+
"id": "integer",
|
|
8
|
+
"street": "virtual",
|
|
9
|
+
"zip": "virtual",
|
|
10
|
+
"city": "virtual",
|
|
11
|
+
"country_code": "virtual",
|
|
12
|
+
"lat": "virtual",
|
|
13
|
+
"lng": "virtual",
|
|
14
|
+
"created_at": "datetime",
|
|
15
|
+
"updated_at": "datetime"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"Contact": {
|
|
19
|
+
"resource_name": "contacts",
|
|
20
|
+
"attributes": {
|
|
21
|
+
"id": "integer",
|
|
22
|
+
"first_name": "virtual",
|
|
23
|
+
"last_name": "virtual",
|
|
24
|
+
"email": "virtual",
|
|
25
|
+
"phone": "virtual",
|
|
26
|
+
"url": "virtual",
|
|
27
|
+
"created_at": "datetime",
|
|
28
|
+
"updated_at": "datetime"
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"Event": {
|
|
32
|
+
"resource_name": "events",
|
|
33
|
+
"attributes": {
|
|
34
|
+
"id": "integer",
|
|
35
|
+
"event_detail_id": "integer",
|
|
36
|
+
"canceled": "virtual",
|
|
37
|
+
"title": "virtual",
|
|
38
|
+
"price_id": "integer",
|
|
39
|
+
"open_for_enrollment": "virtual",
|
|
40
|
+
"enrollment_deadline_at": "datetime",
|
|
41
|
+
"enrollment_form_url": "virtual",
|
|
42
|
+
"address_id": "integer",
|
|
43
|
+
"created_at": "datetime",
|
|
44
|
+
"updated_at": "datetime"
|
|
45
|
+
},
|
|
46
|
+
"associations": {
|
|
47
|
+
"event_detail": {
|
|
48
|
+
"type": "belongs_to",
|
|
49
|
+
"class_name": "EventDetail",
|
|
50
|
+
"inline": false
|
|
51
|
+
},
|
|
52
|
+
"price": {
|
|
53
|
+
"type": "belongs_to",
|
|
54
|
+
"class_name": "Price",
|
|
55
|
+
"inline": true
|
|
56
|
+
},
|
|
57
|
+
"address": {
|
|
58
|
+
"type": "has_one",
|
|
59
|
+
"class_name": "Address",
|
|
60
|
+
"inline": true
|
|
61
|
+
},
|
|
62
|
+
"event_parts": {
|
|
63
|
+
"type": "has_many",
|
|
64
|
+
"class_name": "EventPart",
|
|
65
|
+
"inline": true
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
"EventDetail": {
|
|
70
|
+
"resource_name": "event_details",
|
|
71
|
+
"attributes": {
|
|
72
|
+
"id": "integer",
|
|
73
|
+
"title": "virtual",
|
|
74
|
+
"body": "virtual",
|
|
75
|
+
"image_url": "virtual",
|
|
76
|
+
"slug": "virtual",
|
|
77
|
+
"event_type": "virtual",
|
|
78
|
+
"product_ids": "integer_array",
|
|
79
|
+
"position": "virtual",
|
|
80
|
+
"created_at": "datetime",
|
|
81
|
+
"updated_at": "datetime"
|
|
82
|
+
},
|
|
83
|
+
"associations": {
|
|
84
|
+
"products": {
|
|
85
|
+
"type": "has_and_belongs_to_many",
|
|
86
|
+
"class_name": "Product",
|
|
87
|
+
"inline": false
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
"EventPart": {
|
|
92
|
+
"resource_name": "event_parts",
|
|
93
|
+
"attributes": {
|
|
94
|
+
"id": "integer",
|
|
95
|
+
"event_id": "integer",
|
|
96
|
+
"title": "virtual",
|
|
97
|
+
"starts_at": "datetime",
|
|
98
|
+
"ends_at": "datetime",
|
|
99
|
+
"created_at": "datetime",
|
|
100
|
+
"updated_at": "datetime"
|
|
101
|
+
},
|
|
102
|
+
"associations": {
|
|
103
|
+
"event": {
|
|
104
|
+
"type": "belongs_to",
|
|
105
|
+
"class_name": "Event",
|
|
106
|
+
"inline": false
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
"Instructor": {
|
|
111
|
+
"resource_name": "instructors",
|
|
112
|
+
"attributes": {
|
|
113
|
+
"id": "integer",
|
|
114
|
+
"first_name": "virtual",
|
|
115
|
+
"last_name": "virtual",
|
|
116
|
+
"created_at": "datetime",
|
|
117
|
+
"updated_at": "datetime"
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
"InstructorDetail": {
|
|
121
|
+
"resource_name": "instructor_details",
|
|
122
|
+
"attributes": {
|
|
123
|
+
"id": "integer",
|
|
124
|
+
"instructor_id": "integer",
|
|
125
|
+
"default": "virtual",
|
|
126
|
+
"name": "virtual",
|
|
127
|
+
"avatar_url": "virtual",
|
|
128
|
+
"skills": "virtual",
|
|
129
|
+
"created_at": "datetime",
|
|
130
|
+
"updated_at": "datetime"
|
|
131
|
+
},
|
|
132
|
+
"associations": {
|
|
133
|
+
"instructor": {
|
|
134
|
+
"type": "belongs_to",
|
|
135
|
+
"class_name": "Instructor",
|
|
136
|
+
"inline": true
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
"InstructorGroup": {
|
|
141
|
+
"resource_name": "instructor_groups",
|
|
142
|
+
"attributes": {
|
|
143
|
+
"id": "integer",
|
|
144
|
+
"name": "virtual",
|
|
145
|
+
"position": "virtual",
|
|
146
|
+
"location_id": "integer",
|
|
147
|
+
"created_at": "datetime",
|
|
148
|
+
"updated_at": "datetime"
|
|
149
|
+
},
|
|
150
|
+
"associations": {
|
|
151
|
+
"location": {
|
|
152
|
+
"type": "belongs_to",
|
|
153
|
+
"class_name": "Location",
|
|
154
|
+
"inline": false
|
|
155
|
+
},
|
|
156
|
+
"instructor_group_members": {
|
|
157
|
+
"type": "has_many",
|
|
158
|
+
"class_name": "InstructorGroupMember",
|
|
159
|
+
"inline": true
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
"InstructorGroupMember": {
|
|
164
|
+
"resource_name": "instructor_group_members",
|
|
165
|
+
"attributes": {
|
|
166
|
+
"id": "integer",
|
|
167
|
+
"instructor_group_id": "integer",
|
|
168
|
+
"instructor_detail_id": "integer",
|
|
169
|
+
"position": "virtual",
|
|
170
|
+
"created_at": "datetime",
|
|
171
|
+
"updated_at": "datetime"
|
|
172
|
+
},
|
|
173
|
+
"associations": {
|
|
174
|
+
"instructor_group": {
|
|
175
|
+
"type": "belongs_to",
|
|
176
|
+
"class_name": "InstructorGroup",
|
|
177
|
+
"inline": false
|
|
178
|
+
},
|
|
179
|
+
"instructor_detail": {
|
|
180
|
+
"type": "belongs_to",
|
|
181
|
+
"class_name": "InstructorDetail",
|
|
182
|
+
"inline": true
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
},
|
|
186
|
+
"Location": {
|
|
187
|
+
"resource_name": "locations",
|
|
188
|
+
"attributes": {
|
|
189
|
+
"id": "integer",
|
|
190
|
+
"archived": "virtual",
|
|
191
|
+
"address_id": "integer",
|
|
192
|
+
"contact_id": "integer",
|
|
193
|
+
"connected": "virtual",
|
|
194
|
+
"page": "has_one",
|
|
195
|
+
"created_at": "datetime",
|
|
196
|
+
"updated_at": "datetime"
|
|
197
|
+
},
|
|
198
|
+
"associations": {
|
|
199
|
+
"address": {
|
|
200
|
+
"type": "has_one",
|
|
201
|
+
"class_name": "Address",
|
|
202
|
+
"inline": true
|
|
203
|
+
},
|
|
204
|
+
"contact": {
|
|
205
|
+
"type": "has_one",
|
|
206
|
+
"class_name": "Contact",
|
|
207
|
+
"inline": true
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
},
|
|
211
|
+
"LocationPage": {
|
|
212
|
+
"resource_name": "location_pages",
|
|
213
|
+
"attributes": {
|
|
214
|
+
"id": "integer",
|
|
215
|
+
"location_id": "integer",
|
|
216
|
+
"slug": "virtual",
|
|
217
|
+
"body": "virtual",
|
|
218
|
+
"image_url": "virtual",
|
|
219
|
+
"amenities": "virtual",
|
|
220
|
+
"published": "virtual",
|
|
221
|
+
"created_at": "datetime",
|
|
222
|
+
"updated_at": "datetime"
|
|
223
|
+
},
|
|
224
|
+
"associations": {
|
|
225
|
+
"location": {
|
|
226
|
+
"type": "belongs_to",
|
|
227
|
+
"class_name": "Location",
|
|
228
|
+
"inline": false
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
},
|
|
232
|
+
"Post": {
|
|
233
|
+
"resource_name": "posts",
|
|
234
|
+
"attributes": {
|
|
235
|
+
"id": "integer",
|
|
236
|
+
"title": "virtual",
|
|
237
|
+
"body": "virtual",
|
|
238
|
+
"slug": "virtual",
|
|
239
|
+
"published": "virtual",
|
|
240
|
+
"published_at": "datetime",
|
|
241
|
+
"image_url": "virtual",
|
|
242
|
+
"meta": "virtual",
|
|
243
|
+
"created_at": "datetime",
|
|
244
|
+
"updated_at": "datetime"
|
|
245
|
+
}
|
|
246
|
+
},
|
|
247
|
+
"Price": {
|
|
248
|
+
"resource_name": "prices",
|
|
249
|
+
"attributes": {
|
|
250
|
+
"id": "integer",
|
|
251
|
+
"amount": "amount",
|
|
252
|
+
"currency": "virtual",
|
|
253
|
+
"description": "virtual",
|
|
254
|
+
"price_type": "virtual",
|
|
255
|
+
"interval": "virtual",
|
|
256
|
+
"interval_count": "virtual",
|
|
257
|
+
"default": "virtual",
|
|
258
|
+
"trial": "virtual",
|
|
259
|
+
"created_at": "datetime",
|
|
260
|
+
"updated_at": "datetime"
|
|
261
|
+
}
|
|
262
|
+
},
|
|
263
|
+
"Product": {
|
|
264
|
+
"resource_name": "products",
|
|
265
|
+
"attributes": {
|
|
266
|
+
"id": "integer",
|
|
267
|
+
"name": "virtual",
|
|
268
|
+
"description": "virtual",
|
|
269
|
+
"min_age": "virtual",
|
|
270
|
+
"max_age": "virtual",
|
|
271
|
+
"gender": "virtual",
|
|
272
|
+
"position": "virtual",
|
|
273
|
+
"archived": "virtual",
|
|
274
|
+
"connected": "virtual",
|
|
275
|
+
"created_at": "datetime",
|
|
276
|
+
"updated_at": "datetime"
|
|
277
|
+
},
|
|
278
|
+
"associations": {
|
|
279
|
+
"prices": {
|
|
280
|
+
"type": "has_many",
|
|
281
|
+
"class_name": "Price",
|
|
282
|
+
"inline": true
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
},
|
|
286
|
+
"Session": {
|
|
287
|
+
"resource_name": "sessions",
|
|
288
|
+
"attributes": {
|
|
289
|
+
"id": "integer",
|
|
290
|
+
"location_id": "integer",
|
|
291
|
+
"product_id": "integer",
|
|
292
|
+
"type": "virtual",
|
|
293
|
+
"day_id": "integer",
|
|
294
|
+
"start_time": "time",
|
|
295
|
+
"end_time": "time",
|
|
296
|
+
"start_date": "date",
|
|
297
|
+
"end_date": "date",
|
|
298
|
+
"min_age": "virtual",
|
|
299
|
+
"max_age": "virtual",
|
|
300
|
+
"grade": "virtual",
|
|
301
|
+
"program": "virtual",
|
|
302
|
+
"created_at": "datetime",
|
|
303
|
+
"updated_at": "datetime"
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
},
|
|
307
|
+
"endpoints": {
|
|
308
|
+
"schema": [
|
|
309
|
+
{
|
|
310
|
+
"action": "show",
|
|
311
|
+
"verb": "GET",
|
|
312
|
+
"path": "/2026-07-02/schema"
|
|
313
|
+
}
|
|
314
|
+
],
|
|
315
|
+
"locations": [
|
|
316
|
+
{
|
|
317
|
+
"action": "index",
|
|
318
|
+
"verb": "GET",
|
|
319
|
+
"path": "/2026-07-02/locations"
|
|
320
|
+
},
|
|
321
|
+
{
|
|
322
|
+
"action": "show",
|
|
323
|
+
"verb": "GET",
|
|
324
|
+
"path": "/2026-07-02/locations/:id"
|
|
325
|
+
}
|
|
326
|
+
],
|
|
327
|
+
"products": [
|
|
328
|
+
{
|
|
329
|
+
"action": "index",
|
|
330
|
+
"verb": "GET",
|
|
331
|
+
"path": "/2026-07-02/products"
|
|
332
|
+
},
|
|
333
|
+
{
|
|
334
|
+
"action": "show",
|
|
335
|
+
"verb": "GET",
|
|
336
|
+
"path": "/2026-07-02/products/:id"
|
|
337
|
+
}
|
|
338
|
+
],
|
|
339
|
+
"sessions": [
|
|
340
|
+
{
|
|
341
|
+
"action": "index",
|
|
342
|
+
"verb": "GET",
|
|
343
|
+
"path": "/2026-07-02/sessions"
|
|
344
|
+
},
|
|
345
|
+
{
|
|
346
|
+
"action": "show",
|
|
347
|
+
"verb": "GET",
|
|
348
|
+
"path": "/2026-07-02/sessions/:id"
|
|
349
|
+
}
|
|
350
|
+
],
|
|
351
|
+
"events": [
|
|
352
|
+
{
|
|
353
|
+
"action": "index",
|
|
354
|
+
"verb": "GET",
|
|
355
|
+
"path": "/2026-07-02/events"
|
|
356
|
+
},
|
|
357
|
+
{
|
|
358
|
+
"action": "show",
|
|
359
|
+
"verb": "GET",
|
|
360
|
+
"path": "/2026-07-02/events/:id"
|
|
361
|
+
}
|
|
362
|
+
],
|
|
363
|
+
"event_details": [
|
|
364
|
+
{
|
|
365
|
+
"action": "index",
|
|
366
|
+
"verb": "GET",
|
|
367
|
+
"path": "/2026-07-02/event_details"
|
|
368
|
+
},
|
|
369
|
+
{
|
|
370
|
+
"action": "show",
|
|
371
|
+
"verb": "GET",
|
|
372
|
+
"path": "/2026-07-02/event_details/:id"
|
|
373
|
+
}
|
|
374
|
+
],
|
|
375
|
+
"posts": [
|
|
376
|
+
{
|
|
377
|
+
"action": "index",
|
|
378
|
+
"verb": "GET",
|
|
379
|
+
"path": "/2026-07-02/posts"
|
|
380
|
+
},
|
|
381
|
+
{
|
|
382
|
+
"action": "show",
|
|
383
|
+
"verb": "GET",
|
|
384
|
+
"path": "/2026-07-02/posts/:id"
|
|
385
|
+
}
|
|
386
|
+
],
|
|
387
|
+
"instructor_groups": [
|
|
388
|
+
{
|
|
389
|
+
"action": "index",
|
|
390
|
+
"verb": "GET",
|
|
391
|
+
"path": "/2026-07-02/instructor_groups"
|
|
392
|
+
},
|
|
393
|
+
{
|
|
394
|
+
"action": "show",
|
|
395
|
+
"verb": "GET",
|
|
396
|
+
"path": "/2026-07-02/instructor_groups/:id"
|
|
397
|
+
}
|
|
398
|
+
]
|
|
399
|
+
}
|
|
400
|
+
}
|
metadata
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: keinaufwand
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Codegestalt
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-09-10 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: activesupport
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '7.2'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - ">="
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '7.2'
|
|
27
|
+
description:
|
|
28
|
+
email:
|
|
29
|
+
executables: []
|
|
30
|
+
extensions: []
|
|
31
|
+
extra_rdoc_files: []
|
|
32
|
+
files:
|
|
33
|
+
- README.md
|
|
34
|
+
- lib/keinaufwand.rb
|
|
35
|
+
- lib/keinaufwand/api.rb
|
|
36
|
+
- lib/keinaufwand/api/client.rb
|
|
37
|
+
- lib/keinaufwand/api/client/error.rb
|
|
38
|
+
- lib/keinaufwand/api/client/list.rb
|
|
39
|
+
- lib/keinaufwand/api/client/record.rb
|
|
40
|
+
- lib/keinaufwand/api/configuration.rb
|
|
41
|
+
- lib/keinaufwand/api/schema.rb
|
|
42
|
+
- lib/keinaufwand/api/version.rb
|
|
43
|
+
- schemas/2026-07-02.json
|
|
44
|
+
homepage: https://keinaufwand.com
|
|
45
|
+
licenses: []
|
|
46
|
+
metadata:
|
|
47
|
+
allowed_push_host: https://rubygems.org
|
|
48
|
+
post_install_message:
|
|
49
|
+
rdoc_options: []
|
|
50
|
+
require_paths:
|
|
51
|
+
- lib
|
|
52
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
53
|
+
requirements:
|
|
54
|
+
- - ">="
|
|
55
|
+
- !ruby/object:Gem::Version
|
|
56
|
+
version: '3.2'
|
|
57
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - ">="
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '0'
|
|
62
|
+
requirements: []
|
|
63
|
+
rubygems_version: 3.0.3.1
|
|
64
|
+
signing_key:
|
|
65
|
+
specification_version: 4
|
|
66
|
+
summary: Schema-aware Keinaufwand API client
|
|
67
|
+
test_files: []
|