onoffice_sdk 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: e90e22cbea56ed44b807826226353a8d45bb71269e94579c2e201c68b8513ad0
4
+ data.tar.gz: 8b88aa66df26aa055510af7af24a56495434b18b2d78caef599901985d0275af
5
+ SHA512:
6
+ metadata.gz: 9b6bb076780f3c46930df2ba8c26bbee7d95aa88768d9f04418e8521c5b7558784fafd3943ee08e2cd41c69db56c560469135dbf0f39679b8a5e5292a97b7a35
7
+ data.tar.gz: 944d8601530766b1d2457cbf06aee806f48133e3e704c3190e6ae8f735a8498953d312d076ae4c7877aea515bf549b5e1b6fa2a5e6dc2ec021ae143bda6e3e4b
data/README.md ADDED
@@ -0,0 +1,200 @@
1
+ # onoffice_sdk (Ruby)
2
+
3
+ Ruby client for the onOffice API.
4
+
5
+ - HTTP over TLS
6
+ - Token + HMAC (v2) signing per action
7
+ - Batched requests
8
+ - Pluggable cache hooks
9
+
10
+ ## Install
11
+
12
+ Build and install locally:
13
+
14
+ ```
15
+ gem build onoffice_sdk.gemspec
16
+ gem install onoffice_sdk-*.gem
17
+ ```
18
+
19
+ Add to your Gemfile:
20
+
21
+ ```
22
+ # For local development using this repo
23
+ gem 'onoffice_sdk', path: '.'
24
+
25
+ # Or use the released gem
26
+ # gem 'onoffice_sdk', '~> 0.x'
27
+ ```
28
+
29
+ ## Quickstart
30
+
31
+ ```ruby
32
+ require 'onoffice_sdk'
33
+
34
+ sdk = OnOfficeSDK::SDK.new
35
+ sdk.set_api_version('stable')
36
+
37
+ parameters_read_estate = {
38
+ 'data' => ['Id', 'kaufpreis', 'lage'],
39
+ 'listlimit' => 10,
40
+ 'sortby' => { 'kaufpreis' => 'ASC', 'warmmiete' => 'ASC' },
41
+ 'filter' => {
42
+ 'kaufpreis' => [{ 'op' => '>', 'val' => 300_000 }],
43
+ 'status' => [{ 'op' => '=', 'val' => 1 }]
44
+ }
45
+ }
46
+
47
+ handle = sdk.call_generic(OnOfficeSDK::SDK::ACTION_ID_READ, 'estate', parameters_read_estate)
48
+
49
+ sdk.send_requests('PUT_TOKEN_HERE', 'PUT_SECRET_HERE')
50
+
51
+ p sdk.get_response_array(handle)
52
+ ```
53
+
54
+ ## API
55
+
56
+ - `set_api_version(version)` – sets API version (default: `stable`).
57
+ - `set_api_server(url)` – sets base server (default: `https://api.onoffice.de/api/`).
58
+ - `set_http_options(hash)` – Net::HTTP options, e.g., `{ open_timeout: 5, read_timeout: 30 }`.
59
+ - `call_generic(action_id, resource_type, parameters)` – queue an action.
60
+ - `call(action_id, resource_id, identifier, resource_type, parameters)` – queue an action with explicit identifiers.
61
+ - `send_requests(token, secret)` – send all queued actions in a single HTTP request.
62
+ - `get_response_array(handle)` – fetch and remove the response for a prior handle.
63
+ - `add_cache(cache)` / `set_caches([...])` – register cache backends.
64
+ - `remove_cache_instances` – clear all registered caches.
65
+ - `errors` – returns a hash of errors for failed actions.
66
+
67
+ ### Cache interface
68
+
69
+ Implementors should respond to:
70
+
71
+ ```ruby
72
+ class MyCache
73
+ include OnOfficeSDK::Cache::Interface
74
+
75
+ def initialize(options = {}); end
76
+ def get_http_response_by_parameter_array(parameters); end # -> String (JSON) or nil
77
+ def write(parameters, value); end # value is a JSON string
78
+ def cleanup; end
79
+ def clear_all; end
80
+ end
81
+ ```
82
+
83
+ When a response is cacheable, the SDK writes a JSON string of the response payload to caches. Reads should return that JSON string for a hit.
84
+
85
+ ## Notes
86
+
87
+ - HMAC v2 implemented as Base64.strict_encode64(HMAC-SHA256(secret, timestamp + token + resourcetype + actionid)).
88
+ - Batch request body shape: `{ token, request: { actions: [...] } }`.
89
+ - Response handling uses `status.errorcode`, `data`, and `cacheable`.
90
+
91
+ ## License
92
+
93
+ MIT (see repository LICENSE).
94
+
95
+ ## Rails Integration
96
+
97
+ The gem includes a Railtie for auto-configuration, an optional Rails.cache adapter, and a generator.
98
+
99
+ 1) Gemfile
100
+
101
+ ```
102
+ # For local development using this repo
103
+ gem 'onoffice_sdk', path: '.'
104
+
105
+ # Or use the released gem
106
+ # gem 'onoffice_sdk', '~> 0.x'
107
+ ```
108
+
109
+ 2) Credentials or ENV
110
+
111
+ Set `ONOFFICE_TOKEN` and `ONOFFICE_SECRET` (or use Rails credentials):
112
+
113
+ ```
114
+ # .env or deployment env
115
+ ONOFFICE_TOKEN=your_token
116
+ ONOFFICE_SECRET=your_secret
117
+ ONOFFICE_API_VERSION=stable
118
+ ONOFFICE_API_BASE=https://api.onoffice.de/api/
119
+ ```
120
+
121
+ 3) Initializer (auto-config via Railtie or generator)
122
+
123
+ ```ruby
124
+ OnOfficeSDK.configure do |c|
125
+ # c.api_server = ENV.fetch('ONOFFICE_API_BASE', 'https://api.onoffice.de/api/')
126
+ # c.api_version = ENV.fetch('ONOFFICE_API_VERSION', 'stable')
127
+ # c.open_timeout = 5
128
+ # c.read_timeout = 30
129
+ # c.token = Rails.application.credentials.dig(:onoffice, :token)
130
+ # c.secret = Rails.application.credentials.dig(:onoffice, :secret)
131
+ # c.use_rails_cache = true
132
+ # c.rails_cache_ttl = 600
133
+ end
134
+ ```
135
+
136
+ 4) Use the generator (optional)
137
+
138
+ ```
139
+ bin/rails g onoffice_sdk:install
140
+ ```
141
+
142
+ This creates `config/initializers/onoffice_sdk.rb` and `app/services/onoffice_client.rb`.
143
+
144
+ 5) Example service (app/services/onoffice_client.rb)
145
+
146
+ ```ruby
147
+ class OnofficeClient
148
+ def initialize(sdk: OnOfficeSDK.client)
149
+ @sdk = sdk
150
+ end
151
+
152
+ def read_estates(limit: 10)
153
+ params = { 'data' => ['Id', 'kaufpreis'], 'listlimit' => limit }
154
+ handle = @sdk.call_generic(OnOfficeSDK::SDK::ACTION_ID_READ, 'estate', params)
155
+ @sdk.send_requests(token, secret)
156
+ @sdk.get_response_array(handle)
157
+ end
158
+
159
+ private
160
+
161
+ def token
162
+ ENV.fetch('ONOFFICE_TOKEN')
163
+ end
164
+
165
+ def secret
166
+ ENV.fetch('ONOFFICE_SECRET')
167
+ end
168
+ end
169
+ ```
170
+
171
+ 5) Use in a controller
172
+
173
+ ```ruby
174
+ class EstatesController < ApplicationController
175
+ def index
176
+ @result = OnofficeClient.new.read_estates(limit: 10)
177
+ end
178
+ end
179
+ ```
180
+
181
+ 6) Optional: Rails.cache adapter
182
+
183
+ ```ruby
184
+ require 'digest/md5'
185
+
186
+ Built-in: `OnOfficeSDK::Cache::RailsCache`.
187
+
188
+ ```ruby
189
+ # In the initializer
190
+ # OnOfficeSDK.configure do |c|
191
+ # c.use_rails_cache = true
192
+ # c.rails_cache_ttl = 10.minutes
193
+ # end
194
+ ```
195
+
196
+ Notes for Rails
197
+ - The client is stateless; create once and reuse per-process or inject per-request.
198
+ - `send_requests(token, secret)` batches all queued actions; queue multiple calls, then send once.
199
+ - Cache interface expects JSON strings; example above writes/reads raw strings to `Rails.cache`.
200
+ - ActiveSupport::Notifications: emits `onoffice_sdk.request` around each HTTP call.
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators'
4
+
5
+ module OnofficeSdk
6
+ module Generators
7
+ class InstallGenerator < Rails::Generators::Base
8
+ source_root File.expand_path('templates', __dir__)
9
+
10
+ desc 'Creates an initializer for OnOfficeSDK and an optional service class'
11
+
12
+ class_option :service, type: :boolean, default: true, desc: 'Generate a simple service class using the SDK'
13
+
14
+ def create_initializer
15
+ template 'initializer.rb', 'config/initializers/onoffice_sdk.rb'
16
+ end
17
+
18
+ def create_service
19
+ return unless options[:service]
20
+
21
+ template 'service.rb', 'app/services/onoffice_client.rb'
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ OnOfficeSDK.configure do |c|
4
+ # Configure via ENV, Rails credentials, or set here
5
+ # c.api_server = ENV.fetch('ONOFFICE_API_BASE', 'https://api.onoffice.de/api/')
6
+ # c.api_version = ENV.fetch('ONOFFICE_API_VERSION', 'stable')
7
+ # c.open_timeout = 5
8
+ # c.read_timeout = 30
9
+ # c.token = Rails.application.credentials.dig(:onoffice, :token)
10
+ # c.secret = Rails.application.credentials.dig(:onoffice, :secret)
11
+ # c.use_rails_cache = true
12
+ # c.rails_cache_ttl = 600
13
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ class OnofficeClient
4
+ def initialize(sdk: OnOfficeSDK.client)
5
+ @sdk = sdk
6
+ end
7
+
8
+ def read_estates(limit: 10)
9
+ params = { 'data' => %w[Id kaufpreis], 'listlimit' => limit }
10
+ handle = @sdk.call_generic(OnOfficeSDK::SDK::ACTION_ID_READ, 'estate', params)
11
+ @sdk.send_requests(token, secret)
12
+ @sdk.get_response_array(handle)
13
+ end
14
+
15
+ private
16
+
17
+ def token
18
+ OnOfficeSDK.configuration.token || ENV.fetch('ONOFFICE_TOKEN', nil)
19
+ end
20
+
21
+ def secret
22
+ OnOfficeSDK.configuration.secret || ENV.fetch('ONOFFICE_SECRET', nil)
23
+ end
24
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OnOfficeSDK
4
+ module Cache
5
+ # Informal interface for cache backends.
6
+ # Implementers should provide the following methods:
7
+ # - initialize(options = {})
8
+ # - get_http_response_by_parameter_array(parameters) -> String or nil
9
+ # - write(parameters, value) -> true/false
10
+ # - cleanup
11
+ # - clear_all
12
+ module Interface
13
+ def get_http_response_by_parameter_array(_parameters); end
14
+ def write(_parameters, _value); end
15
+ def cleanup; end
16
+ def clear_all; end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'digest/md5'
4
+
5
+ module OnOfficeSDK
6
+ module Cache
7
+ class RailsCache
8
+ include OnOfficeSDK::Cache::Interface
9
+
10
+ def initialize(options = {})
11
+ @namespace = options[:namespace] || 'onoffice'
12
+ @ttl = options[:ttl] || 300
13
+ end
14
+
15
+ def get_http_response_by_parameter_array(parameters)
16
+ return nil unless defined?(Rails)
17
+
18
+ Rails.cache.read(key(parameters))
19
+ end
20
+
21
+ def write(parameters, value)
22
+ return false unless defined?(Rails)
23
+
24
+ Rails.cache.write(key(parameters), value, expires_in: @ttl)
25
+ end
26
+
27
+ def cleanup; end
28
+
29
+ def clear_all
30
+ return unless defined?(Rails)
31
+
32
+ # Best-effort namespaced clear
33
+ Rails.cache.delete_matched("#{@namespace}:*") if Rails.cache.respond_to?(:delete_matched)
34
+ end
35
+
36
+ private
37
+
38
+ def key(parameters)
39
+ digest = Digest::MD5.hexdigest(Marshal.dump(parameters))
40
+ "#{@namespace}:#{digest}"
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OnOfficeSDK
4
+ class Configuration
5
+ attr_accessor :api_server, :api_version, :open_timeout, :read_timeout,
6
+ :token, :secret, :use_rails_cache, :rails_cache_ttl
7
+
8
+ def initialize
9
+ @api_server = 'https://api.onoffice.de/api/'
10
+ @api_version = 'stable'
11
+ @open_timeout = 5
12
+ @read_timeout = 30
13
+ @token = nil
14
+ @secret = nil
15
+ @use_rails_cache = false
16
+ @rails_cache_ttl = 300 # seconds
17
+ end
18
+ end
19
+
20
+ class << self
21
+ def configuration
22
+ @configuration ||= Configuration.new
23
+ end
24
+
25
+ def configure
26
+ yield(configuration)
27
+ refresh_default_client!
28
+ end
29
+
30
+ def client
31
+ @client ||= build_client_from_config
32
+ end
33
+
34
+ def refresh_default_client!
35
+ @client = build_client_from_config
36
+ end
37
+
38
+ private
39
+
40
+ def build_client_from_config
41
+ sdk = OnOfficeSDK::SDK.new
42
+ cfg = configuration
43
+ sdk.set_api_server(cfg.api_server)
44
+ sdk.set_api_version(cfg.api_version)
45
+ sdk.set_http_options(open_timeout: cfg.open_timeout, read_timeout: cfg.read_timeout)
46
+ if cfg.use_rails_cache && defined?(Rails)
47
+ begin
48
+ require_relative 'cache/rails_cache'
49
+ sdk.add_cache(OnOfficeSDK::Cache::RailsCache.new(ttl: cfg.rails_cache_ttl))
50
+ rescue LoadError
51
+ # ignore if Rails cache adapter not available
52
+ end
53
+ end
54
+ sdk
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OnOfficeSDK
4
+ class SDKError < StandardError; end
5
+
6
+ class ApiCallFaultyResponseError < SDKError; end
7
+ class ApiCallNoActionParametersError < SDKError; end
8
+
9
+ class HttpFetchNoResultError < SDKError
10
+ attr_accessor :errno
11
+ end
12
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OnOfficeSDK
4
+ module Internal
5
+ class ApiAction
6
+ def initialize(action_id, resource_type, parameters, resource_id = '', identifier = '', timestamp = nil)
7
+ sorted_params = parameters.is_a?(Hash) ? parameters.sort.to_h : parameters
8
+ @action_parameters = {
9
+ 'actionid' => action_id,
10
+ 'identifier' => identifier,
11
+ 'parameters' => sorted_params,
12
+ 'resourceid' => resource_id,
13
+ 'resourcetype' => resource_type,
14
+ 'timestamp' => timestamp
15
+ }
16
+ end
17
+
18
+ attr_reader :action_parameters
19
+
20
+ def identifier
21
+ # Deterministic identifier for caching
22
+ require 'digest/md5'
23
+ Digest::MD5.hexdigest(Marshal.dump(@action_parameters))
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,167 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module OnOfficeSDK
6
+ module Internal
7
+ class ApiCall
8
+ def initialize
9
+ @request_queue = {}
10
+ @responses = {}
11
+ @errors = {}
12
+ @api_version = 'stable'
13
+ @caches = []
14
+ @server = nil
15
+ @http_options = {}
16
+ end
17
+
18
+ def call_by_raw_data(action_id, resource_id, identifier, resource_type, parameters = {})
19
+ p_api_action = ApiAction.new(action_id, resource_type, parameters, resource_id, identifier)
20
+ p_request = Request.new(p_api_action)
21
+ request_id = p_request.request_id
22
+ @request_queue[request_id] = p_request
23
+ request_id
24
+ end
25
+
26
+ def send_requests(token, secret, http_fetch = nil)
27
+ collect_or_gather_requests(token, secret, http_fetch)
28
+ end
29
+
30
+ def set_http_options(opts)
31
+ @http_options = opts || {}
32
+ end
33
+
34
+ def get_response(handle)
35
+ if @responses.key?(handle)
36
+ p_response = @responses[handle]
37
+ raise ApiCallFaultyResponseError, "Handle: #{handle}" unless p_response.valid?
38
+
39
+ @responses.delete(handle)
40
+ return p_response.response_data
41
+ end
42
+ nil
43
+ end
44
+
45
+ def set_api_version(v)
46
+ @api_version = v
47
+ end
48
+
49
+ def set_server(server)
50
+ @server = server
51
+ end
52
+
53
+ attr_reader :errors
54
+
55
+ def add_cache(cache)
56
+ @caches << cache
57
+ end
58
+
59
+ def remove_cache_instances
60
+ @caches = []
61
+ end
62
+
63
+ private
64
+
65
+ def collect_or_gather_requests(token, secret, http_fetch)
66
+ action_parameters = []
67
+ action_parameters_order = []
68
+
69
+ @request_queue.each_value do |p_request|
70
+ used_parameters = p_request.api_action.action_parameters
71
+ cached_response = get_from_cache(used_parameters)
72
+ if cached_response.nil?
73
+ parameters_this_action = p_request.create_request(token, secret)
74
+ action_parameters << parameters_this_action
75
+ action_parameters_order << p_request
76
+ else
77
+ @responses[p_request.request_id] = Response.new(p_request, cached_response)
78
+ end
79
+ end
80
+
81
+ send_http_requests(token, action_parameters, action_parameters_order, http_fetch)
82
+ @request_queue = {}
83
+ end
84
+
85
+ def send_http_requests(token, action_parameters, action_parameters_order, http_fetch)
86
+ return if action_parameters.empty?
87
+
88
+ response_http = get_from_http(token, action_parameters, http_fetch)
89
+ result = JSON.parse(response_http)
90
+ raise HttpFetchNoResultError unless result.dig('response', 'results')
91
+
92
+ ids_for_cache = []
93
+
94
+ result['response']['results'].each_with_index do |result_http, request_number|
95
+ p_request = action_parameters_order[request_number]
96
+ request_id = p_request.request_id
97
+ if result_http.dig('status', 'errorcode').to_i.zero?
98
+ @responses[request_id] = Response.new(p_request, result_http)
99
+ ids_for_cache << request_id
100
+ else
101
+ @errors[request_id] = result_http
102
+ end
103
+ end
104
+
105
+ write_cache_for_responses(ids_for_cache)
106
+ end
107
+
108
+ def get_from_http(token, action_parameters, http_fetch)
109
+ request = {
110
+ 'token' => token,
111
+ 'request' => { 'actions' => action_parameters }
112
+ }
113
+ if http_fetch.nil?
114
+ http_fetch = HttpFetch.new(api_url, JSON.generate(request))
115
+ http_fetch.http_options = @http_options
116
+ end
117
+ if defined?(ActiveSupport::Notifications)
118
+ ActiveSupport::Notifications.instrument('onoffice_sdk.request',
119
+ url: api_url,
120
+ actions_count: action_parameters.size) do
121
+ http_fetch.send
122
+ end
123
+ else
124
+ http_fetch.send
125
+ end
126
+ end
127
+
128
+ def write_cache_for_responses(response_ids)
129
+ return if @caches.empty?
130
+
131
+ response_objects = @responses.slice(*response_ids).values
132
+ response_objects.each do |p_response|
133
+ next unless p_response.cacheable?
134
+
135
+ response_data = p_response.response_data
136
+ request_parameters = p_response.request.api_action.action_parameters
137
+ write_cache(JSON.generate(response_data), request_parameters)
138
+ end
139
+ end
140
+
141
+ def write_cache(result, action_parameters)
142
+ @caches.each { |cache| cache.write(action_parameters, result) }
143
+ end
144
+
145
+ def get_from_cache(parameters)
146
+ @caches.each do |cache|
147
+ result_cache = cache.get_http_response_by_parameter_array(parameters)
148
+ next if result_cache.nil?
149
+
150
+ begin
151
+ return JSON.parse(result_cache)
152
+ rescue JSON::ParserError
153
+ # ignore invalid cache content
154
+ end
155
+ end
156
+ nil
157
+ end
158
+
159
+ def api_url
160
+ raise SDKError, 'Server not set' unless @server
161
+
162
+ base = @server.end_with?('/') ? @server : "#{@server}/"
163
+ "#{base}#{URI.encode_www_form_component(@api_version)}/api.php"
164
+ end
165
+ end
166
+ end
167
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net/http'
4
+ require 'uri'
5
+
6
+ module OnOfficeSDK
7
+ module Internal
8
+ class HttpFetch
9
+ def initialize(url, post_data)
10
+ @url = url
11
+ @post_data = post_data
12
+ @http_options = {}
13
+ end
14
+
15
+ def http_options=(opts)
16
+ @http_options = opts || {}
17
+ end
18
+
19
+ def send
20
+ uri = URI.parse(@url)
21
+ http = Net::HTTP.new(uri.host, uri.port)
22
+ http.use_ssl = (uri.scheme == 'https')
23
+ # Timeouts / options
24
+ http.open_timeout = @http_options[:open_timeout] if @http_options[:open_timeout]
25
+ http.read_timeout = @http_options[:read_timeout] if @http_options[:read_timeout]
26
+
27
+ req = Net::HTTP::Post.new(uri.request_uri)
28
+ req['Content-Type'] = 'application/json'
29
+ req['Accept-Encoding'] = 'gzip,deflate,br'
30
+ req.body = @post_data
31
+
32
+ res = http.request(req)
33
+ body = res&.body
34
+ unless res.is_a?(Net::HTTPSuccess) && body
35
+ err = HttpFetchNoResultError.new(res&.code.to_s)
36
+ err.errno = nil
37
+ raise err
38
+ end
39
+ body
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'base64'
5
+ require 'openssl'
6
+
7
+ module OnOfficeSDK
8
+ module Internal
9
+ class Request
10
+ @request_id_static = 0
11
+ class << self
12
+ attr_accessor :request_id_static
13
+ end
14
+
15
+ def initialize(api_action)
16
+ @api_action = api_action
17
+ @request_id = (self.class.request_id_static ||= 0)
18
+ self.class.request_id_static += 1
19
+ end
20
+
21
+ def create_request(token, secret)
22
+ action_parameters = @api_action.action_parameters.dup
23
+ action_parameters['timestamp'] ||= Time.now.to_i
24
+ action_parameters['hmac_version'] = 2
25
+
26
+ action_id = action_parameters['actionid']
27
+ type = action_parameters['resourcetype']
28
+ hmac = create_hmac2(token, secret, action_parameters['timestamp'], type, action_id)
29
+ action_parameters['hmac'] = hmac
30
+
31
+ action_parameters
32
+ end
33
+
34
+ def create_hmac2(token, secret, timestamp, type, action_id)
35
+ fields = {
36
+ 'timestamp' => timestamp,
37
+ 'token' => token,
38
+ 'resourcetype' => type,
39
+ 'actionid' => action_id
40
+ }
41
+ raw = fields.values.join
42
+ digest = OpenSSL::HMAC.digest('sha256', secret.to_s, raw)
43
+ Base64.strict_encode64(digest)
44
+ end
45
+
46
+ attr_reader :request_id, :api_action
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OnOfficeSDK
4
+ module Internal
5
+ class Response
6
+ def initialize(request, response_data)
7
+ @request = request
8
+ @response_data = response_data
9
+ end
10
+
11
+ def valid?
12
+ @response_data.is_a?(Hash) &&
13
+ @response_data.key?('actionid') &&
14
+ @response_data.key?('resourcetype') &&
15
+ @response_data.key?('data')
16
+ end
17
+
18
+ def cacheable?
19
+ valid? && @response_data['cacheable']
20
+ end
21
+
22
+ attr_reader :request, :response_data
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/railtie'
4
+
5
+ module OnOfficeSDK
6
+ class Railtie < ::Rails::Railtie
7
+ config.onoffice_sdk = ActiveSupport::OrderedOptions.new
8
+
9
+ initializer 'onoffice_sdk.configure' do |app|
10
+ cfg = app.config.onoffice_sdk
11
+
12
+ OnOfficeSDK.configure do |c|
13
+ c.api_server = cfg.api_server || ENV['ONOFFICE_API_BASE'] || c.api_server
14
+ c.api_version = cfg.api_version || ENV['ONOFFICE_API_VERSION'] || c.api_version
15
+ c.open_timeout = cfg.open_timeout || c.open_timeout
16
+ c.read_timeout = cfg.read_timeout || c.read_timeout
17
+
18
+ c.token = cfg.token || begin
19
+ Rails.application.credentials.dig(:onoffice,
20
+ :token)
21
+ rescue StandardError
22
+ nil
23
+ end || ENV.fetch('ONOFFICE_TOKEN', nil)
24
+ c.secret = cfg.secret || begin
25
+ Rails.application.credentials.dig(:onoffice,
26
+ :secret)
27
+ rescue StandardError
28
+ nil
29
+ end || ENV.fetch('ONOFFICE_SECRET', nil)
30
+
31
+ c.use_rails_cache = cfg.fetch(:use_rails_cache, c.use_rails_cache)
32
+ c.rails_cache_ttl = cfg.rails_cache_ttl || c.rails_cache_ttl
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OnOfficeSDK
4
+ # Main SDK client for the onOffice API.
5
+ class SDK
6
+ ACTION_ID_READ = 'urn:onoffice-de-ns:smart:2.5:smartml:action:read'
7
+ ACTION_ID_CREATE = 'urn:onoffice-de-ns:smart:2.5:smartml:action:create'
8
+ ACTION_ID_MODIFY = 'urn:onoffice-de-ns:smart:2.5:smartml:action:modify'
9
+ ACTION_ID_GET = 'urn:onoffice-de-ns:smart:2.5:smartml:action:get'
10
+ ACTION_ID_DO = 'urn:onoffice-de-ns:smart:2.5:smartml:action:do'
11
+ ACTION_ID_DELETE = 'urn:onoffice-de-ns:smart:2.5:smartml:action:delete'
12
+
13
+ RELATION_TYPE_BUYER = 'urn:onoffice-de-ns:smart:2.5:relationTypes:estate:address:buyer'
14
+ RELATION_TYPE_TENANT = 'urn:onoffice-de-ns:smart:2.5:relationTypes:estate:address:renter'
15
+ RELATION_TYPE_OWNER = 'urn:onoffice-de-ns:smart:2.5:relationTypes:estate:address:owner'
16
+ MODULE_ADDRESS = 'address'
17
+ MODULE_ESTATE = 'estate'
18
+ MODULE_SEARCHCRITERIA = 'searchcriteria'
19
+ RELATION_TYPE_CONTACT_BROKER = 'urn:onoffice-de-ns:smart:2.5:relationTypes:estate:address:contactPerson'
20
+ RELATION_TYPE_CONTACT_PERSON = 'urn:onoffice-de-ns:smart:2.5:relationTypes:estate:address:contactPersonAll'
21
+ RELATION_TYPE_COMPLEX_ESTATE_UNITS = 'urn:onoffice-de-ns:smart:2.5:relationTypes:complex:estate:units'
22
+ RELATION_TYPE_ESTATE_ADDRESS_OWNER = 'urn:onoffice-de-ns:smart:2.5:relationTypes:estate:address:owner'
23
+
24
+ def initialize(api_call: nil)
25
+ @api_call = api_call || Internal::ApiCall.new
26
+ @api_call.set_server('https://api.onoffice.de/api/')
27
+ end
28
+
29
+ def set_api_version(api_version)
30
+ @api_call.set_api_version(api_version)
31
+ end
32
+
33
+ def set_api_server(server)
34
+ @api_call.set_server(server)
35
+ end
36
+
37
+ def set_http_options(options)
38
+ @api_call.set_http_options(options)
39
+ end
40
+
41
+ def call_generic(action_id, resource_type, parameters)
42
+ @api_call.call_by_raw_data(action_id, '', '', resource_type, parameters)
43
+ end
44
+
45
+ def call(action_id, resource_id, identifier, resource_type, parameters)
46
+ @api_call.call_by_raw_data(action_id, resource_id, identifier, resource_type, parameters)
47
+ end
48
+
49
+ def send_requests(token, secret)
50
+ @api_call.send_requests(token, secret)
51
+ end
52
+
53
+ def get_response_array(number)
54
+ @api_call.get_response(number)
55
+ end
56
+
57
+ def add_cache(cache)
58
+ @api_call.add_cache(cache)
59
+ end
60
+
61
+ def set_caches(cache_instances)
62
+ Array(cache_instances).each { |c| @api_call.add_cache(c) }
63
+ end
64
+
65
+ def remove_cache_instances
66
+ @api_call.remove_cache_instances
67
+ end
68
+
69
+ def errors
70
+ @api_call.errors
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OnOfficeSDK
4
+ VERSION = '0.1.0'
5
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'net/http'
5
+ require 'uri'
6
+
7
+ require_relative 'onoffice_sdk/version'
8
+ require_relative 'onoffice_sdk/configuration'
9
+ require_relative 'onoffice_sdk/sdk'
10
+ require_relative 'onoffice_sdk/internal/api_call'
11
+ require_relative 'onoffice_sdk/internal/api_action'
12
+ require_relative 'onoffice_sdk/internal/request'
13
+ require_relative 'onoffice_sdk/internal/response'
14
+ require_relative 'onoffice_sdk/internal/http_fetch'
15
+ require_relative 'onoffice_sdk/cache/interface'
16
+ require_relative 'onoffice_sdk/errors'
17
+
18
+ # Auto-load Railtie if in Rails
19
+ begin
20
+ require_relative 'onoffice_sdk/railtie' if defined?(Rails::Railtie)
21
+ rescue LoadError
22
+ # ignore when Rails is not present
23
+ end
metadata ADDED
@@ -0,0 +1,131 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: onoffice_sdk
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - onOffice GmbH
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2025-11-20 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: json
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '2.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '2.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rspec
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '3.12'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '3.12'
41
+ - !ruby/object:Gem::Dependency
42
+ name: simplecov
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '0.21'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '0.21'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rubocop
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.48'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.48'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rubocop-rspec
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '2.20'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '2.20'
83
+ description: Lightweight Ruby client to communicate with the onOffice API, supporting
84
+ batched requests, HMAC signing, and pluggable caching.
85
+ email:
86
+ - support@onoffice.de
87
+ executables: []
88
+ extensions: []
89
+ extra_rdoc_files: []
90
+ files:
91
+ - README.md
92
+ - lib/generators/onoffice_sdk/install/install_generator.rb
93
+ - lib/generators/onoffice_sdk/install/templates/initializer.rb
94
+ - lib/generators/onoffice_sdk/install/templates/service.rb
95
+ - lib/onoffice_sdk.rb
96
+ - lib/onoffice_sdk/cache/interface.rb
97
+ - lib/onoffice_sdk/cache/rails_cache.rb
98
+ - lib/onoffice_sdk/configuration.rb
99
+ - lib/onoffice_sdk/errors.rb
100
+ - lib/onoffice_sdk/internal/api_action.rb
101
+ - lib/onoffice_sdk/internal/api_call.rb
102
+ - lib/onoffice_sdk/internal/http_fetch.rb
103
+ - lib/onoffice_sdk/internal/request.rb
104
+ - lib/onoffice_sdk/internal/response.rb
105
+ - lib/onoffice_sdk/railtie.rb
106
+ - lib/onoffice_sdk/sdk.rb
107
+ - lib/onoffice_sdk/version.rb
108
+ homepage: https://apidoc.onoffice.de/
109
+ licenses:
110
+ - MIT
111
+ metadata: {}
112
+ post_install_message:
113
+ rdoc_options: []
114
+ require_paths:
115
+ - lib
116
+ required_ruby_version: !ruby/object:Gem::Requirement
117
+ requirements:
118
+ - - ">="
119
+ - !ruby/object:Gem::Version
120
+ version: '2.6'
121
+ required_rubygems_version: !ruby/object:Gem::Requirement
122
+ requirements:
123
+ - - ">="
124
+ - !ruby/object:Gem::Version
125
+ version: '0'
126
+ requirements: []
127
+ rubygems_version: 3.2.3
128
+ signing_key:
129
+ specification_version: 4
130
+ summary: Ruby client for the onOffice API
131
+ test_files: []