data_nexus 0.2.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: 3a77e46a3dbaca91a51fa4b1eda5f5f5aa267b1f8c7011d21888de892b19e373
4
+ data.tar.gz: 91e3d85cdec6629444eaf4c5a7846be3dbaf876ec00b56cefc12b2bac18b473c
5
+ SHA512:
6
+ metadata.gz: 3fe4cf797a07bdff2cbe1f1a8ad7689f4ea2ff29396c75896e01acf05e41fe88c51f8f7965655565b8e7078c5205e1eb3aa31d5a8bcb31e26133189db958fe51
7
+ data.tar.gz: 575e20457eca50ebbaf258b9fad1d86210a78337c8bfa04a535457843ce83a73687e21f5629e28a2bdb755a7592a38e4745839a54bb830ded1bae3a8670bc0c2
@@ -0,0 +1,43 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ jobs:
10
+ test:
11
+ runs-on: ubuntu-latest
12
+
13
+ strategy:
14
+ fail-fast: false
15
+ matrix:
16
+ ruby-version: ["3.0", "3.1", "3.2", "3.4", "4.0"]
17
+
18
+ steps:
19
+ - uses: actions/checkout@v4
20
+
21
+ - name: Set up Ruby ${{ matrix.ruby-version }}
22
+ uses: ruby/setup-ruby@v1
23
+ with:
24
+ ruby-version: ${{ matrix.ruby-version }}
25
+ bundler-cache: true
26
+
27
+ - name: Run tests
28
+ run: bundle exec rspec
29
+
30
+ lint:
31
+ runs-on: ubuntu-latest
32
+
33
+ steps:
34
+ - uses: actions/checkout@v4
35
+
36
+ - name: Set up Ruby
37
+ uses: ruby/setup-ruby@v1
38
+ with:
39
+ ruby-version: "3.2"
40
+ bundler-cache: true
41
+
42
+ - name: Run RuboCop
43
+ run: bundle exec rubocop
@@ -0,0 +1,14 @@
1
+ # Copy this file to .mise.local.toml and fill in real values
2
+ # This file is gitignored and should contain your local dev/test credentials
3
+
4
+ [env]
5
+ # The vast majority of these are for testing against a local DataNexus instance and recording VCR cassettes.
6
+ DATANEXUS_API_KEY = "your-real-api-key"
7
+ DATANEXUS_BASE_URL = "https://localhost:4000"
8
+ DATANEXUS_SSL_VERIFY = "false"
9
+ DATANEXUS_TEST_PROGRAM_ID = "your-real-program-id"
10
+ DATANEXUS_TEST_MEMBER_ID = "your-real-member-id"
11
+ DATANEXUS_TEST_CONSENT_ID = "your-real-consent-id"
12
+ DATANEXUS_TEST_ENROLLMENT_ID = "your-real-enrollment-id"
13
+ DATANEXUS_TEST_BORN_ON = "1980-01-15"
14
+ DATANEXUS_TEST_EMPLOYEE_ID = "EMP123"
data/.mise.toml ADDED
@@ -0,0 +1,36 @@
1
+ [tools]
2
+ ruby = "3.3"
3
+
4
+ [tasks.setup]
5
+ description = "Install dependencies"
6
+ run = "bundle install"
7
+
8
+ [tasks.test]
9
+ description = "Run all tests"
10
+ run = "bundle exec rspec --exclude-pattern 'spec/integration/**/*'"
11
+ depends = ["setup"]
12
+
13
+ [tasks."test:all"]
14
+ description = "Run all tests including integration"
15
+ run = "bundle exec rspec"
16
+ depends = ["setup"]
17
+
18
+ [tasks."test:integration"]
19
+ description = "Run integration tests only"
20
+ run = "bundle exec rspec spec/integration/"
21
+ depends = ["setup"]
22
+
23
+ [tasks.lint]
24
+ description = "Run rubocop"
25
+ run = "bundle exec rubocop"
26
+ depends = ["setup"]
27
+
28
+ [tasks.fix]
29
+ description = "Run rubocop with auto-fix"
30
+ run = "bundle exec rubocop -a"
31
+ depends = ["setup"]
32
+
33
+ [tasks.console]
34
+ description = "Start IRB console with gem loaded"
35
+ run = "bundle exec bin/console"
36
+ depends = ["setup"]
data/.rubocop.yml ADDED
@@ -0,0 +1,28 @@
1
+ plugins:
2
+ - rubocop-rake
3
+ - rubocop-rspec
4
+
5
+ AllCops:
6
+ TargetRubyVersion: 3.0
7
+ NewCops: enable
8
+
9
+ # Integration tests have different expectations
10
+ RSpec/DescribeClass:
11
+ Exclude:
12
+ - "spec/integration/**/*"
13
+
14
+ RSpec/MultipleExpectations:
15
+ Exclude:
16
+ - "spec/integration/**/*"
17
+
18
+ RSpec/ExampleLength:
19
+ Exclude:
20
+ - "spec/integration/**/*"
21
+
22
+ RSpec/MultipleMemoizedHelpers:
23
+ Exclude:
24
+ - "spec/integration/**/*"
25
+
26
+ Metrics/ParameterLists:
27
+ Exclude:
28
+ - "lib/data_nexus/client.rb"
data/README.md ADDED
@@ -0,0 +1,268 @@
1
+ # DataNexus
2
+
3
+ Ruby client for the DataNexus API.
4
+
5
+ ## Installation
6
+
7
+ Install from GitHub (before gem is published):
8
+
9
+ ```ruby
10
+ gem 'data_nexus', git: 'https://github.com/DartHealth/datanexus-ruby.git', tag: 'v0.1.0'
11
+ ```
12
+
13
+ Or from RubyGems (once published):
14
+
15
+ ```ruby
16
+ gem 'data_nexus'
17
+ ```
18
+
19
+ ## Usage
20
+
21
+ ```ruby
22
+ client = DataNexus::Client.new(
23
+ api_key: ENV['DATANEXUS_API_KEY'],
24
+ base_url: 'https://api.datanexus.com' # optional
25
+ )
26
+ ```
27
+
28
+ ## Program Members
29
+
30
+ ### List Members
31
+
32
+ Filters are required. Valid combinations:
33
+ - `born_on` + `employee_id`
34
+ - `born_on` + `first_name` + `last_name`
35
+ - `born_on` + `first_name_prefix` + `last_name_prefix`
36
+
37
+ ```ruby
38
+ collection = client.programs('program-id').members.list(
39
+ born_on: '1980-01-15',
40
+ employee_id: 'EMP123'
41
+ )
42
+
43
+ collection.data.each do |member|
44
+ puts "#{member[:first_name]} #{member[:last_name]}"
45
+ end
46
+ ```
47
+
48
+ ### Search Members
49
+
50
+ Search for members within a program. Returns a bounded result set (max 10 results) with a `more_results` flag indicating if additional matches exist.
51
+
52
+ Valid parameter combinations:
53
+ - `born_on` + `employee_id`
54
+ - `born_on` + `first_name` + `last_name`
55
+ - `born_on` + `first_name` + `last_name` + `employee_id`
56
+ - `born_on` + `first_name_prefix` + `last_name_prefix`
57
+ - `born_on` + `first_name_prefix` + `last_name_prefix` + `employee_id`
58
+
59
+ ```ruby
60
+ # Search by employee ID and DOB
61
+ result = client.programs('program-id').search_members(
62
+ born_on: '1980-01-15',
63
+ employee_id: 'EMP123'
64
+ )
65
+
66
+ result[:data].each do |member|
67
+ puts "#{member[:first_name]} #{member[:last_name]}"
68
+ end
69
+
70
+ puts "More results available" if result[:more_results]
71
+
72
+ # Search by name and DOB
73
+ result = client.programs('program-id').search_members(
74
+ born_on: '1980-01-15',
75
+ first_name: 'George',
76
+ last_name: 'Washington'
77
+ )
78
+
79
+ # Search by name prefix and DOB
80
+ result = client.programs('program-id').search_members(
81
+ born_on: '1980-01-15',
82
+ first_name_prefix: 'G',
83
+ last_name_prefix: 'Was'
84
+ )
85
+ ```
86
+
87
+ Note: Unlike `list`, `search_members` does not support pagination. It returns up to 10 results with a `more_results` boolean. An `ArgumentError` will be raised if an invalid parameter combination is provided.
88
+
89
+ Note: Depending on your API key, `search_members` may be the only method you have access to. Contact your DataNexus representative for more information about your API key's permissions.
90
+
91
+ ### Pagination
92
+
93
+ ```ruby
94
+ collection.each_page do |page|
95
+ page.data.each { |member| process(member) }
96
+ end
97
+
98
+ # Or iterate all records directly
99
+ collection.each { |member| process(member) }
100
+
101
+ # Manual pagination
102
+ if collection.next_page?
103
+ next_collection = collection.next_page
104
+ end
105
+ ```
106
+
107
+ ### Find Member
108
+
109
+ ```ruby
110
+ member = client.programs('program-id').members('member-id').find
111
+ puts member[:first_name]
112
+ ```
113
+
114
+ ### Update Member
115
+
116
+ ```ruby
117
+ response = client.programs('program-id').members('member-id').update(
118
+ member: { phone_number: '+15551234567' }
119
+ )
120
+ ```
121
+
122
+ ### Household Members
123
+
124
+ ```ruby
125
+ household = client.programs('program-id').members('member-id').household
126
+ household.each { |member| puts member[:first_name] }
127
+ ```
128
+
129
+ ### Member Consents
130
+
131
+ #### Create Consent
132
+
133
+ ```ruby
134
+ response = client.programs('program-id').members('member-id').consents.create(
135
+ consent: {
136
+ category: 'sms',
137
+ member_response: true,
138
+ consent_details: { sms_phone_number: '+15558675309' }
139
+ }
140
+ )
141
+ # program_id is automatically injected
142
+ ```
143
+
144
+ #### Find Consent
145
+
146
+ ```ruby
147
+ consent = client.programs('program-id').members('member-id').consents.find(123)
148
+ puts consent[:category]
149
+ ```
150
+
151
+ #### Update Consent
152
+
153
+ ```ruby
154
+ response = client.programs('program-id').members('member-id').consents.update(123,
155
+ consent: { member_response: false }
156
+ )
157
+ ```
158
+
159
+ #### Delete Consent
160
+
161
+ ```ruby
162
+ client.programs('program-id').members('member-id').consents.delete(123)
163
+ ```
164
+
165
+ ### Member Enrollments
166
+
167
+ #### Create Enrollment
168
+
169
+ ```ruby
170
+ response = client.programs('program-id').members('member-id').enrollments.create(
171
+ enrollment: {
172
+ enrolled_at: '2024-01-01T00:00:00Z',
173
+ expires_at: '2025-01-01T00:00:00Z'
174
+ }
175
+ )
176
+ # program_id is automatically injected
177
+ ```
178
+
179
+ #### Find Enrollment
180
+
181
+ ```ruby
182
+ enrollment = client.programs('program-id').members('member-id').enrollments.find(123)
183
+ puts enrollment[:enrolled_at]
184
+ ```
185
+
186
+ #### Update Enrollment
187
+
188
+ ```ruby
189
+ response = client.programs('program-id').members('member-id').enrollments.update(123,
190
+ enrollment: { expires_at: '2026-01-01T00:00:00Z' }
191
+ )
192
+ ```
193
+
194
+ #### Delete Enrollment
195
+
196
+ ```ruby
197
+ client.programs('program-id').members('member-id').enrollments.delete(123)
198
+ ```
199
+
200
+ ## Top-Level Members
201
+
202
+ You can also access members without a program scope:
203
+
204
+ ### List Members
205
+
206
+ ```ruby
207
+ collection = client.members.list(
208
+ first_name: 'George',
209
+ last_name: 'Washington',
210
+ born_on: '1976-07-04'
211
+ )
212
+
213
+ # Filter by program eligibility
214
+ collection = client.members.list(program_id: 'program-uuid')
215
+
216
+ # Filter by update time
217
+ collection = client.members.list(updated_since: '2024-01-01T00:00:00Z')
218
+
219
+ # Pagination
220
+ collection = client.members.list(first: 50, after: 'cursor')
221
+ ```
222
+
223
+ ### Find Member
224
+
225
+ ```ruby
226
+ member = client.members.find('member-id')
227
+ puts member[:first_name]
228
+ ```
229
+
230
+ ### Update Member
231
+
232
+ ```ruby
233
+ response = client.members.update('member-id',
234
+ member: { phone_number: '+15551234567' }
235
+ )
236
+ ```
237
+
238
+ ## Error Handling
239
+
240
+ ```ruby
241
+ begin
242
+ client.programs('id').members('id').find
243
+ rescue DataNexus::AuthenticationError
244
+ # 401
245
+ rescue DataNexus::NotFoundError
246
+ # 404
247
+ rescue DataNexus::UnprocessableEntityError
248
+ # 422
249
+ rescue DataNexus::RateLimitError => e
250
+ sleep(e.retry_after)
251
+ rescue DataNexus::APIError => e
252
+ puts "#{e.status}: #{e.message}"
253
+ end
254
+ ```
255
+
256
+ ## Development
257
+
258
+ ```bash
259
+ cp .mise.local.toml.example .mise.local.toml
260
+ # Edit .mise.local.toml with your test credentials
261
+ bundle install
262
+ bundle exec rspec
263
+ bundle exec rubocop
264
+ ```
265
+
266
+ ## License
267
+
268
+ MIT
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'configuration'
4
+ require_relative 'connection'
5
+ require_relative 'resources/members'
6
+ require_relative 'resources/programs'
7
+
8
+ module DataNexus
9
+ # Main client for interacting with the DataNexus API
10
+ #
11
+ # @example Create a client with explicit configuration
12
+ # client = DataNexus::Client.new(api_key: "your_api_key")
13
+ #
14
+ # @example Create a client with a configuration object
15
+ # config = DataNexus::Configuration.new(api_key: "your_api_key")
16
+ # client = DataNexus::Client.new(config: config)
17
+ #
18
+ # @example Access program members
19
+ # client.programs("program-uuid").members.list
20
+ # client.programs("program-uuid").members("member-id").find
21
+ #
22
+ class Client
23
+ # @return [Configuration] The client configuration
24
+ attr_reader :config
25
+
26
+ # @return [Connection] The HTTP connection
27
+ attr_reader :connection
28
+
29
+ # Initialize a new Client
30
+ #
31
+ # @param api_key [String, nil] The API key for authentication
32
+ # @param base_url [String, nil] The base URL for the API
33
+ # @param timeout [Integer, nil] Request timeout in seconds
34
+ # @param open_timeout [Integer, nil] Connection open timeout in seconds
35
+ # @param config [Configuration, nil] A pre-built configuration object
36
+ #
37
+ # @raise [ConfigurationError] If no API key is provided
38
+ def initialize(api_key: nil, base_url: nil, timeout: nil, open_timeout: nil, ssl_verify: nil, config: nil)
39
+ @config = config || build_configuration(
40
+ api_key: api_key,
41
+ base_url: base_url,
42
+ timeout: timeout,
43
+ open_timeout: open_timeout,
44
+ ssl_verify: ssl_verify
45
+ )
46
+
47
+ validate_configuration!
48
+
49
+ @connection = Connection.new(@config)
50
+ end
51
+
52
+ # Access top-level member resources
53
+ #
54
+ # @return [Resources::Members] A members resource
55
+ #
56
+ # @example
57
+ # client.members.list
58
+ # client.members.find("member-id")
59
+ def members
60
+ Resources::Members.new(connection)
61
+ end
62
+
63
+ # Access program-scoped resources
64
+ #
65
+ # @param program_id [String] The program UUID
66
+ # @return [Resources::Programs] A program resource proxy
67
+ #
68
+ # @example
69
+ # client.programs("uuid").members.list
70
+ def programs(program_id)
71
+ Resources::Programs.new(connection, program_id)
72
+ end
73
+
74
+ private
75
+
76
+ # Build a configuration from individual parameters
77
+ #
78
+ # @param api_key [String, nil]
79
+ # @param base_url [String, nil]
80
+ # @param timeout [Integer, nil]
81
+ # @param open_timeout [Integer, nil]
82
+ # @param ssl_verify [Boolean, nil]
83
+ # @return [Configuration]
84
+ def build_configuration(api_key:, base_url:, timeout:, open_timeout:, ssl_verify:)
85
+ Configuration.new(
86
+ api_key: api_key,
87
+ base_url: base_url || Configuration::DEFAULT_BASE_URL,
88
+ timeout: timeout || Configuration::DEFAULT_TIMEOUT,
89
+ open_timeout: open_timeout || Configuration::DEFAULT_OPEN_TIMEOUT,
90
+ ssl_verify: ssl_verify.nil? ? Configuration::DEFAULT_SSL_VERIFY : ssl_verify
91
+ )
92
+ end
93
+
94
+ # Validate that the configuration is complete
95
+ #
96
+ # @raise [ConfigurationError] If the configuration is invalid
97
+ def validate_configuration!
98
+ return if @config.valid?
99
+
100
+ raise ConfigurationError, 'API key is required. Pass api_key: or provide a configured Configuration object.'
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DataNexus
4
+ # Wrapper for paginated API responses
5
+ #
6
+ # Provides convenience methods for accessing data and navigating through pages.
7
+ # The underlying data remains as hashes - this class just adds pagination helpers.
8
+ #
9
+ # @example Accessing data
10
+ # collection = client.programs('uuid').members.list
11
+ # collection.data.each { |member| puts member[:first_name] }
12
+ #
13
+ # @example Manual pagination
14
+ # collection = client.programs('uuid').members.list(first: 50)
15
+ # while collection
16
+ # process(collection.data)
17
+ # collection = collection.next_page
18
+ # end
19
+ #
20
+ # @example Block pagination
21
+ # client.programs('uuid').members.list(first: 50).each_page do |page|
22
+ # page.data.each { |member| puts member[:first_name] }
23
+ # end
24
+ #
25
+ class Collection
26
+ # @return [Array<Hash>] The records in this page
27
+ attr_reader :data
28
+
29
+ # @return [String, nil] Cursor for the start of this page
30
+ attr_reader :start_cursor
31
+
32
+ # @return [String, nil] Cursor for the end of this page
33
+ attr_reader :end_cursor
34
+
35
+ # Initialize a new Collection
36
+ #
37
+ # @param response [Hash] The raw API response
38
+ # @param resource [Object] The resource instance that made the request
39
+ # @param params [Hash] The params used for the original request
40
+ def initialize(response, resource:, params:)
41
+ @data = response[:data] || []
42
+ @start_cursor = response[:start_cursor]
43
+ @end_cursor = response[:end_cursor]
44
+ @resource = resource
45
+ @params = params
46
+ end
47
+
48
+ # Check if there's a next page of results
49
+ #
50
+ # @return [Boolean]
51
+ def next_page?
52
+ !end_cursor.nil? && !end_cursor.empty?
53
+ end
54
+
55
+ # Fetch the next page of results
56
+ #
57
+ # @return [Collection, nil] The next page, or nil if no more pages
58
+ def next_page
59
+ return nil unless next_page?
60
+
61
+ @resource.list(**@params, after: end_cursor)
62
+ end
63
+
64
+ # Check if there's a previous page of results
65
+ #
66
+ # @return [Boolean]
67
+ def previous_page?
68
+ !start_cursor.nil? && !start_cursor.empty?
69
+ end
70
+
71
+ # Fetch the previous page of results
72
+ #
73
+ # @return [Collection, nil] The previous page, or nil if no more pages
74
+ def previous_page
75
+ return nil unless previous_page?
76
+
77
+ @resource.list(**@params, before: start_cursor)
78
+ end
79
+
80
+ # Iterate through all pages starting from this one
81
+ #
82
+ # @yield [Collection] Each page of results
83
+ # @return [Enumerator] If no block given
84
+ def each_page
85
+ return enum_for(:each_page) unless block_given?
86
+
87
+ page = self
88
+ while page
89
+ yield page
90
+ page = page.next_page
91
+ end
92
+ end
93
+
94
+ # Iterate through all records across all pages
95
+ #
96
+ # @yield [Hash] Each record
97
+ # @return [Enumerator] If no block given
98
+ def each_record(&block)
99
+ return enum_for(:each_record) unless block_given?
100
+
101
+ each_page do |page|
102
+ page.data.each(&block)
103
+ end
104
+ end
105
+
106
+ alias each each_record
107
+
108
+ # @return [Boolean] Whether this page has any records
109
+ def empty?
110
+ data.empty?
111
+ end
112
+
113
+ # @return [Integer] Number of records in this page
114
+ def size
115
+ data.size
116
+ end
117
+
118
+ alias length size
119
+ end
120
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DataNexus
4
+ # Configuration class for storing API credentials and settings
5
+ #
6
+ # @example Configure the client globally
7
+ # DataNexus.configure do |config|
8
+ # config.api_key = "your_api_key"
9
+ # config.base_url = "https://api.datanexus.com"
10
+ # end
11
+ #
12
+ # @example Create a configuration instance
13
+ # config = DataNexus::Configuration.new(
14
+ # api_key: "your_api_key",
15
+ # base_url: "https://api.datanexus.com"
16
+ # )
17
+ #
18
+ class Configuration
19
+ # @return [String, nil] The API key for authentication
20
+ attr_accessor :api_key
21
+
22
+ # @return [String] The base URL for the DataNexus API
23
+ attr_accessor :base_url
24
+
25
+ # @return [Integer] Request timeout in seconds
26
+ attr_accessor :timeout
27
+
28
+ # @return [Integer] Connection open timeout in seconds
29
+ attr_accessor :open_timeout
30
+
31
+ # @return [Boolean] Whether to verify SSL certificates
32
+ attr_accessor :ssl_verify
33
+
34
+ # Default base URL for the DataNexus API
35
+ DEFAULT_BASE_URL = 'https://api.datanexus.com'
36
+
37
+ # Default request timeout in seconds
38
+ DEFAULT_TIMEOUT = 30
39
+
40
+ # Default connection open timeout in seconds
41
+ DEFAULT_OPEN_TIMEOUT = 10
42
+
43
+ # Default SSL verification setting
44
+ DEFAULT_SSL_VERIFY = true
45
+
46
+ # Initialize a new Configuration instance
47
+ #
48
+ # @param api_key [String, nil] The API key for authentication
49
+ # @param base_url [String] The base URL for the API
50
+ # @param timeout [Integer] Request timeout in seconds
51
+ # @param open_timeout [Integer] Connection open timeout in seconds
52
+ # @param ssl_verify [Boolean] Whether to verify SSL certificates (set to false for self-signed certs)
53
+ def initialize(
54
+ api_key: nil,
55
+ base_url: DEFAULT_BASE_URL,
56
+ timeout: DEFAULT_TIMEOUT,
57
+ open_timeout: DEFAULT_OPEN_TIMEOUT,
58
+ ssl_verify: DEFAULT_SSL_VERIFY
59
+ )
60
+ @api_key = api_key
61
+ @base_url = base_url
62
+ @timeout = timeout
63
+ @open_timeout = open_timeout
64
+ @ssl_verify = ssl_verify
65
+ end
66
+
67
+ # Check if the configuration has valid credentials
68
+ #
69
+ # @return [Boolean] true if api_key is present
70
+ def valid?
71
+ !api_key.nil? && !api_key.empty?
72
+ end
73
+ end
74
+ end