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 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,5 @@
1
+ module Keinaufwand
2
+ module Api
3
+ VERSION = "0.1.0"
4
+ end
5
+ 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
@@ -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: []