integrated_data 0.0.1
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/.gitignore +16 -0
- data/Gemfile +14 -0
- data/LICENSE.txt +22 -0
- data/README.md +277 -0
- data/Rakefile +13 -0
- data/bin/rake +10 -0
- data/integrated_data.gemspec +26 -0
- data/lib/integrated_data.rb +7 -0
- data/lib/integrated_data/entity.rb +43 -0
- data/lib/integrated_data/entity/integrator.rb +43 -0
- data/lib/integrated_data/entity/lookup.rb +52 -0
- data/lib/integrated_data/identifier.rb +9 -0
- data/lib/integrated_data/source.rb +9 -0
- data/test/integrated_data/entity_test.rb +59 -0
- data/test/integrated_data/identifier_test.rb +0 -0
- data/test/integrated_data/source_test.rb +0 -0
- data/test/integrated_data_test.rb +99 -0
- data/test/test_helper.rb +52 -0
- metadata +138 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: feede0b1ad2087f3b7e3402a9ce5fa008fe73581
|
4
|
+
data.tar.gz: 37ebb4022498eedcb5e0abab75fc3128fb8f04c7
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 8a40a762fecc1a8612a26f58bd0ba236b8fee9263d591762b98c108e939e36bee1358f320873949f6f8738e3abf2ef4b8df83cf1636797b53d7af006e919810f
|
7
|
+
data.tar.gz: 1f92981670d5344cd6832dfd7b4a07930bf6c87132690ba6d1f0427ee9aafd6efe76e5d29e1bc673e41daf521cc17ccce35d6c16f57c6d1a8e39706bd0ad71ed
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2015 Chris Keele
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,277 @@
|
|
1
|
+
IntegratedData
|
2
|
+
==============
|
3
|
+
|
4
|
+
> *A tool for compositing custom domain objects from disparate data sources.*
|
5
|
+
|
6
|
+
|
7
|
+
|
8
|
+
Synopsis
|
9
|
+
--------
|
10
|
+
|
11
|
+
`IntegratedData` is a micro-framework for [data integration](http://en.wikipedia.org/wiki/Data_integration) that allows you to build [POROs](http://blog.jayfields.com/2007/10/ruby-poro.html) whose properties are scattered across various sources.
|
12
|
+
|
13
|
+
|
14
|
+
|
15
|
+
Concepts
|
16
|
+
--------
|
17
|
+
|
18
|
+
|
19
|
+
### Data Sources
|
20
|
+
|
21
|
+
An `IntegratedData::Source` is any class capable of retrieving data from some source on initialization. It could access a database, API, CSV, or pretty much anything under the digital sun. Public methods of `IntegratedData::Sources` act like different strategies for accessing the data therein.
|
22
|
+
|
23
|
+
|
24
|
+
### Entities
|
25
|
+
|
26
|
+
An `IntegratedData::Entity` represents a concept with various attributes. Each attribute can specify different data sources and strategies it can be derived from, using different identifiers.
|
27
|
+
|
28
|
+
|
29
|
+
### Identifiers
|
30
|
+
|
31
|
+
Finally, you are encouraged to make proper value objects for your identifiers by using `IntegratedData::Identifier`s. When initializing an entity, any ids with a corresponding identifier class will be coerced into an instance of one of these.
|
32
|
+
|
33
|
+
|
34
|
+
|
35
|
+
API
|
36
|
+
---
|
37
|
+
|
38
|
+
|
39
|
+
### Data Sources
|
40
|
+
|
41
|
+
Convert any class into a data source by extending `IntegratedData::Source`:
|
42
|
+
|
43
|
+
```ruby
|
44
|
+
require 'integrated_data'
|
45
|
+
|
46
|
+
class MySource
|
47
|
+
extend IntegratedData::Source
|
48
|
+
end
|
49
|
+
```
|
50
|
+
|
51
|
+
#### Requirements
|
52
|
+
|
53
|
+
Sources are required to implement these methods:
|
54
|
+
|
55
|
+
- *`IntegratedData::Source.build(options = {})`*
|
56
|
+
|
57
|
+
Data sources must implement a class method, `build`, that can accept a Hash of options and returns an object you can invoke strategies on.
|
58
|
+
|
59
|
+
- *`IntegratedData::Source` strategies*
|
60
|
+
|
61
|
+
The object returned by `IntegratedData::Source.build` should have public methods that accept a single argument–an identifier–and return data from the source.
|
62
|
+
|
63
|
+
#### Results
|
64
|
+
|
65
|
+
- *Hooked in*
|
66
|
+
|
67
|
+
When the class is used as the `:source` parameter of the `IntegratedData::Entity.lookup` DSL, it will be used to fetch data for that attribute.
|
68
|
+
|
69
|
+
#### Extensions
|
70
|
+
|
71
|
+
There are currently no gems that extend `IntegratedData::Source`.
|
72
|
+
|
73
|
+
|
74
|
+
### Identifiers
|
75
|
+
|
76
|
+
Convert any class into an identifier by extending `IntegratedData::Identifier`:
|
77
|
+
|
78
|
+
```ruby
|
79
|
+
require 'integrated_data'
|
80
|
+
|
81
|
+
class MyIdentifier
|
82
|
+
extend IntegratedData::Identifier
|
83
|
+
end
|
84
|
+
```
|
85
|
+
|
86
|
+
#### Requirements
|
87
|
+
|
88
|
+
Entities are required to implement these methods:
|
89
|
+
|
90
|
+
- *`IntegratedData::Identifier#parse(value)`*
|
91
|
+
|
92
|
+
Identifiers must be able to be parse values into a value object.
|
93
|
+
|
94
|
+
#### Results
|
95
|
+
|
96
|
+
- *Hooked in*
|
97
|
+
|
98
|
+
When the class is used as the `:identifier` parameter of the `IntegratedData::Entity.lookup` DSL, it will be used to coerce identifiers into that value object.
|
99
|
+
|
100
|
+
#### Extensions
|
101
|
+
|
102
|
+
There are currently no gems that extend `IntegratedData::Identifier`.
|
103
|
+
|
104
|
+
|
105
|
+
### Entities
|
106
|
+
|
107
|
+
Convert any class into an entity by extending `IntegratedData::Entity`:
|
108
|
+
|
109
|
+
```ruby
|
110
|
+
require 'integrated_data'
|
111
|
+
|
112
|
+
class MyEntity
|
113
|
+
extend IntegratedData::Entity
|
114
|
+
end
|
115
|
+
```
|
116
|
+
|
117
|
+
#### Requirements
|
118
|
+
|
119
|
+
Entities are required to implement these methods:
|
120
|
+
|
121
|
+
- *`IntegratedData::Source#initialize(attributes = {})`*
|
122
|
+
|
123
|
+
Entities must be able to be initialized with a single argument, a Hash of attributes.
|
124
|
+
|
125
|
+
#### Results
|
126
|
+
|
127
|
+
- *Lookup DSL*
|
128
|
+
|
129
|
+
Entities gain a class method, `lookup`, that can be used to register ways to look up attributes. They can be instantiated with a hash of identifiers, and are then automatically initialized with a hash of attributes.
|
130
|
+
|
131
|
+
- *`@identifiers`*
|
132
|
+
|
133
|
+
Entity instances have access to an instance variable, `@identifiers`, reflecting the identifiers they were furnished with on initialization.
|
134
|
+
|
135
|
+
- *`@attributes`*
|
136
|
+
|
137
|
+
Entity instances have access to an instance variable, `@attributes`, reflecting the attributes they were furnished with on initialization.
|
138
|
+
|
139
|
+
#### Extensions
|
140
|
+
|
141
|
+
There are currently no gems that extend `IntegratedData::Entity`.
|
142
|
+
|
143
|
+
|
144
|
+
|
145
|
+
Examples
|
146
|
+
--------
|
147
|
+
|
148
|
+
|
149
|
+
### Sources
|
150
|
+
|
151
|
+
#### CSVs
|
152
|
+
|
153
|
+
An in-memory data source could be implemented as such:
|
154
|
+
|
155
|
+
```ruby
|
156
|
+
class InMemorySource < Array
|
157
|
+
|
158
|
+
# This simply creates a default `build` method that raises an NotImplementedError.
|
159
|
+
extend IntegratedData::Source
|
160
|
+
|
161
|
+
# Here we fulfill the IntegratedData::Source interface
|
162
|
+
# by overriding the class method with our own implementation.
|
163
|
+
class << self
|
164
|
+
# `build` must accept a Hash; in this case we require an array of hashes.
|
165
|
+
def build(data: [])
|
166
|
+
data = Array.try_convert(data).map do |hashlike|
|
167
|
+
Hash.try_convert(data)
|
168
|
+
end.compact
|
169
|
+
new data
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
# Default strategy, if none is specified by the entity
|
174
|
+
def call(id)
|
175
|
+
find do |attributes|
|
176
|
+
id == attributes[:id]
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
end
|
181
|
+
```
|
182
|
+
|
183
|
+
A CSV data source could be implemented as such:
|
184
|
+
|
185
|
+
```ruby
|
186
|
+
require 'smarter_csv'
|
187
|
+
class CSVSource
|
188
|
+
|
189
|
+
# This simply creates a default `build` method that raises an NotImplementedError.
|
190
|
+
extend IntegratedData::Source
|
191
|
+
|
192
|
+
# Here we fulfill the IntegratedData::Source interface
|
193
|
+
# by overriding the class method with our own implementation.
|
194
|
+
class << self
|
195
|
+
# `build` must accept a Hash; in this case we require a file parameter.
|
196
|
+
def build(file:)
|
197
|
+
new(file)
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
# Just store the file for reference so CSVs are only parsed when needed.
|
202
|
+
def initialize(file)
|
203
|
+
@file = file
|
204
|
+
end
|
205
|
+
|
206
|
+
# Default strategy, if none is specified by the entity
|
207
|
+
def call(id)
|
208
|
+
@data ||= SmarterCSV.process(@file)
|
209
|
+
@data.find do |attributes|
|
210
|
+
id == attributes[:id]
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
# Custom strategy with variant behavior, used on demand.
|
215
|
+
# In this case, if an entity needs multiple attributes from the csv file,
|
216
|
+
# and it's using the `:uncached` strategy,
|
217
|
+
# it'll process the csv each time it needs an attribute from it.
|
218
|
+
def uncached(id)
|
219
|
+
SmarterCSV.process(@file).find do |attributes|
|
220
|
+
id == attributes[:id]
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
end
|
225
|
+
```
|
226
|
+
|
227
|
+
|
228
|
+
### Identifiers
|
229
|
+
|
230
|
+
An identifier could be implemented as such:
|
231
|
+
|
232
|
+
```ruby
|
233
|
+
class PaddedString < String
|
234
|
+
|
235
|
+
# This simply creates a default `parse` method that raises an NotImplementedError.
|
236
|
+
extend IntegratedData::Identifier
|
237
|
+
|
238
|
+
# Here we fulfill the IntegratedData::Identifier interface
|
239
|
+
# by overriding the class method with our own implementation.
|
240
|
+
class << self
|
241
|
+
# `parse` must accept a single value to be coerced.
|
242
|
+
def parse(value)
|
243
|
+
string = value.to_s
|
244
|
+
string.insert(0, '0') until string.length >= 10
|
245
|
+
new string
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
# Pad other strings before comparison.
|
250
|
+
def == other
|
251
|
+
super self.class.parse other
|
252
|
+
end
|
253
|
+
|
254
|
+
end
|
255
|
+
```
|
256
|
+
|
257
|
+
|
258
|
+
### Entities
|
259
|
+
|
260
|
+
Finally, we could put it all together as such:
|
261
|
+
|
262
|
+
```ruby
|
263
|
+
require 'ostruct'
|
264
|
+
|
265
|
+
# We use OpenStruct to get an object that can be initialized with a hash of attributes.
|
266
|
+
class Student < OpenStruct
|
267
|
+
|
268
|
+
# This allows you to use the `lookup` DSL and defines `new` to hook into it.
|
269
|
+
extend IntegratedData::Entity
|
270
|
+
|
271
|
+
# The full lookup DSL looks like this:
|
272
|
+
# lookup :attribute_name, from: SourceClass, by: :identifier_key, with: (OptionalIdentifierClass or nil), strategy: (:optional_strategy_method or :call), **extra_options_for_source)
|
273
|
+
|
274
|
+
lookup :first_name, from: InMemorySource, by: :student_id, data: [student_id: '0000000001', first_name: 'Chris']
|
275
|
+
|
276
|
+
end
|
277
|
+
```
|
data/Rakefile
ADDED
data/bin/rake
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = "integrated_data"
|
7
|
+
spec.version = '0.0.1'
|
8
|
+
spec.authors = ["Chris Keele"]
|
9
|
+
spec.email = ["dev@chriskeele.com"]
|
10
|
+
spec.summary = "A tool for compositing custom domain objects from disparate data sources."
|
11
|
+
# spec.description = %q{TODO: Write a longer description. Optional.}
|
12
|
+
spec.homepage = ""
|
13
|
+
spec.license = "MIT"
|
14
|
+
|
15
|
+
spec.files = `git ls-files -z`.split("\x0")
|
16
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
17
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
18
|
+
spec.require_paths = ["lib"]
|
19
|
+
|
20
|
+
spec.add_runtime_dependency "activesupport", ">= 3"
|
21
|
+
|
22
|
+
spec.add_development_dependency "bundler", "~> 1.7"
|
23
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
24
|
+
spec.add_development_dependency 'minitest', '>= 5.0'
|
25
|
+
spec.add_development_dependency 'gem-console'
|
26
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'active_support/core_ext/class/attribute'
|
2
|
+
|
3
|
+
require 'integrated_data/entity/integrator'
|
4
|
+
require 'integrated_data/entity/lookup'
|
5
|
+
|
6
|
+
module IntegratedData
|
7
|
+
module Entity
|
8
|
+
|
9
|
+
class << self
|
10
|
+
def extended base; super
|
11
|
+
base.class_attribute :lookups, instance_accessor: false, instance_predicate: false
|
12
|
+
base.lookups = Set.new
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def new(identifiers={})
|
17
|
+
attributes = integrated(identifiers).attributes
|
18
|
+
super(attributes).tap do |entity|
|
19
|
+
entity.class_eval do
|
20
|
+
@identifiers = identifiers
|
21
|
+
@attributes = attributes
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def lookup(attribute, id: , identifier: nil, source: , strategy: :call, **options, &extractor)
|
27
|
+
self.lookups = lookups.dup.add Lookup.new attribute, id, identifier, source, strategy, extractor, options
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def integrated(identifiers)
|
33
|
+
Integrator.new(identifiers, available_lookups(identifiers.keys))
|
34
|
+
end
|
35
|
+
|
36
|
+
def available_lookups(identifiers)
|
37
|
+
lookups.select do |lookup|
|
38
|
+
identifiers.include? lookup.id
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'integrated_data/entity/lookup'
|
2
|
+
|
3
|
+
module IntegratedData
|
4
|
+
module Entity
|
5
|
+
Integrator = Struct.new(:identifiers, :lookups)
|
6
|
+
class Integrator
|
7
|
+
|
8
|
+
def initialize(*args)
|
9
|
+
@cache = {}
|
10
|
+
super
|
11
|
+
end
|
12
|
+
|
13
|
+
def attributes
|
14
|
+
Hash[
|
15
|
+
lookups.group_by(&:attribute).map do |attribute, lookups|
|
16
|
+
[ attribute, find_attribute_for(lookups, attribute) ]
|
17
|
+
end
|
18
|
+
]
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def perform_lookup(lookup)
|
24
|
+
@cache[lookup.cache_keys] ||= lookup.perform(identifiers[lookup.id])
|
25
|
+
end
|
26
|
+
|
27
|
+
def data_for(lookup, attribute)
|
28
|
+
if data = perform_lookup(lookup)
|
29
|
+
lookup.extract data, attribute
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def find_attribute_for(lookups, attribute)
|
34
|
+
lookups.find do |lookup|
|
35
|
+
data_for(lookup, attribute).tap do |data|
|
36
|
+
return data if data
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module IntegratedData
|
2
|
+
module Entity
|
3
|
+
Lookup = Struct.new(:attribute, :id, :identifier, :source, :strategy, :extractor, :options)
|
4
|
+
class Lookup
|
5
|
+
|
6
|
+
class << self
|
7
|
+
def cache_keys
|
8
|
+
[:identifier, :source, :strategy, :extractor, :options]
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def cache_keys
|
13
|
+
self.class.cache_keys.map do |key|
|
14
|
+
send key
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def perform(value)
|
19
|
+
source.build(options).public_send(strategy, id, prepare(value))
|
20
|
+
end
|
21
|
+
|
22
|
+
def extract(data, attribute)
|
23
|
+
if extractor
|
24
|
+
extractor.call data, attribute
|
25
|
+
else
|
26
|
+
default_extractor data, attribute
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
class Error < Exception; end
|
33
|
+
|
34
|
+
def prepare(value)
|
35
|
+
identifier ? identifier.parse(value) : value
|
36
|
+
end
|
37
|
+
|
38
|
+
def default_extractor(data, attribute)
|
39
|
+
if data.respond_to? :extract
|
40
|
+
data.extract attribute
|
41
|
+
elsif data.respond_to? :to_h
|
42
|
+
data.to_h[attribute]
|
43
|
+
elsif data.respond_to? attribute
|
44
|
+
data.public_send attribute
|
45
|
+
else
|
46
|
+
raise Error, "couldn't extract #{attribute} from #{data.class}"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class EntityTest < Test
|
4
|
+
|
5
|
+
def pre; super
|
6
|
+
@entity_class_name = :TestEntity
|
7
|
+
klass = Class.new do
|
8
|
+
extend IntegratedData::Entity
|
9
|
+
end
|
10
|
+
Object.send :const_set, @entity_class_name, klass
|
11
|
+
@entity_class = Object.send :const_get, @entity_class_name
|
12
|
+
@entity_settings = [:attribute, source: :source, id: :identifier]
|
13
|
+
end
|
14
|
+
|
15
|
+
def post; super
|
16
|
+
Object.send :remove_const, @entity_class_name
|
17
|
+
@entity_class_name = nil
|
18
|
+
@entity_class = nil
|
19
|
+
@entity_settings = nil
|
20
|
+
end
|
21
|
+
|
22
|
+
def test_fetch_defines_new_lookups
|
23
|
+
around do
|
24
|
+
assert_change ->{ @entity_class.lookups.length } do
|
25
|
+
@entity_class.lookup *@entity_settings
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def test_lookups_without_strategies_dont_repeat
|
31
|
+
around do
|
32
|
+
@entity_class.lookup *@entity_settings
|
33
|
+
refute_change ->{ @entity_class.lookups.length } do
|
34
|
+
@entity_class.lookup *@entity_settings
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def test_lookups_with_strategies_dont_repeat
|
40
|
+
around do
|
41
|
+
@entity_settings.last[:strategy] = :strategy
|
42
|
+
@entity_class.lookup *@entity_settings
|
43
|
+
refute_change ->{ @entity_class.lookups.length } do
|
44
|
+
@entity_class.lookup *@entity_settings
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def test_lookups_mixing_strategies_do_repeat
|
50
|
+
around do
|
51
|
+
@entity_class.lookup *@entity_settings
|
52
|
+
assert_change ->{ @entity_class.lookups.length } do
|
53
|
+
@entity_settings.last[:strategy] = :strategy
|
54
|
+
@entity_class.lookup *@entity_settings
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
File without changes
|
File without changes
|
@@ -0,0 +1,99 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class Rxcui < String
|
4
|
+
|
5
|
+
extend IntegratedData::Identifier
|
6
|
+
|
7
|
+
class << self
|
8
|
+
def parse(input)
|
9
|
+
new input.to_s
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def == other
|
14
|
+
super self.class.parse other
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
|
19
|
+
class RxNorm
|
20
|
+
|
21
|
+
extend IntegratedData::Source
|
22
|
+
|
23
|
+
class << self
|
24
|
+
alias_method :build, :new
|
25
|
+
end
|
26
|
+
|
27
|
+
def initialize(data: )
|
28
|
+
@data = data
|
29
|
+
end
|
30
|
+
|
31
|
+
def method_missing(method, id, value)
|
32
|
+
@data[method == :call ? :default : method].find do |datum|
|
33
|
+
value == datum[id]
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
|
39
|
+
require 'ostruct'
|
40
|
+
class Drug < OpenStruct
|
41
|
+
|
42
|
+
extend IntegratedData::Entity
|
43
|
+
|
44
|
+
class << self
|
45
|
+
def data
|
46
|
+
{
|
47
|
+
default: [
|
48
|
+
{
|
49
|
+
rxcui: 1,
|
50
|
+
name: 'name1',
|
51
|
+
extra_name: 'extraname1'
|
52
|
+
},
|
53
|
+
{
|
54
|
+
rxcui: 2,
|
55
|
+
name: 'name2',
|
56
|
+
extra_name: 'extraname2'
|
57
|
+
},
|
58
|
+
],
|
59
|
+
custom: [
|
60
|
+
{
|
61
|
+
rxcui: 1,
|
62
|
+
other_name: 'othername1',
|
63
|
+
extra_name: 'extraname1'
|
64
|
+
},
|
65
|
+
{
|
66
|
+
rxcui: 2,
|
67
|
+
other_name: 'othername2',
|
68
|
+
extra_name: 'extraname2'
|
69
|
+
},
|
70
|
+
],
|
71
|
+
}
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
lookup :name, source: RxNorm, id: :rxcui, identifier: Rxcui, data: data
|
76
|
+
|
77
|
+
lookup :other_name, source: RxNorm, id: :rxcui, identifier: Rxcui, strategy: :custom, data: data
|
78
|
+
|
79
|
+
lookup :extra_name, source: RxNorm, id: :rxcui, identifier: Rxcui, data: data do |data, attribute|
|
80
|
+
data[attribute]
|
81
|
+
end
|
82
|
+
|
83
|
+
end
|
84
|
+
|
85
|
+
class IntegratedDataTest < Test
|
86
|
+
|
87
|
+
def test_default_strategy
|
88
|
+
assert_equal 'name1', Drug.new(rxcui: 1).name
|
89
|
+
end
|
90
|
+
|
91
|
+
def test_custom_strategy
|
92
|
+
assert_equal 'othername1', Drug.new(rxcui: 1).other_name
|
93
|
+
end
|
94
|
+
|
95
|
+
def test_custom_extractor
|
96
|
+
assert_equal 'extraname1', Drug.new(rxcui: 1).extra_name
|
97
|
+
end
|
98
|
+
|
99
|
+
end
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'pry'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'simplecov'
|
6
|
+
SimpleCov.coverage_dir 'coverage'
|
7
|
+
if ENV['CI'] and require 'coveralls'
|
8
|
+
SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter[
|
9
|
+
SimpleCov::Formatter::HTMLFormatter,
|
10
|
+
Coveralls::SimpleCov::Formatter
|
11
|
+
]
|
12
|
+
end
|
13
|
+
SimpleCov.start do
|
14
|
+
add_filter "/test"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
require 'minitest/autorun'
|
19
|
+
|
20
|
+
class Test < MiniTest::Test
|
21
|
+
|
22
|
+
def assert_change(lambda_getter)
|
23
|
+
old = lambda_getter.call
|
24
|
+
yield
|
25
|
+
refute_equal old, lambda_getter.call, "Expected block to change output of lambda argument"
|
26
|
+
end
|
27
|
+
|
28
|
+
def refute_change(lambda_getter)
|
29
|
+
old = lambda_getter.call
|
30
|
+
yield
|
31
|
+
assert_equal old, lambda_getter.call, "Didn't expect block to change output of lambda argument"
|
32
|
+
end
|
33
|
+
|
34
|
+
def setup; end
|
35
|
+
def teardown; end
|
36
|
+
|
37
|
+
class Namespace; end
|
38
|
+
|
39
|
+
def around
|
40
|
+
pre
|
41
|
+
yield
|
42
|
+
post
|
43
|
+
end
|
44
|
+
|
45
|
+
def pre; end
|
46
|
+
def post; end
|
47
|
+
|
48
|
+
end
|
49
|
+
|
50
|
+
require 'integrated_data'
|
51
|
+
|
52
|
+
puts 'Running tests...'
|
metadata
ADDED
@@ -0,0 +1,138 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: integrated_data
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Chris Keele
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-07-23 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: '3'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '3'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: bundler
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.7'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.7'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '10.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '10.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: minitest
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '5.0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '5.0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: gem-console
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
description:
|
84
|
+
email:
|
85
|
+
- dev@chriskeele.com
|
86
|
+
executables:
|
87
|
+
- rake
|
88
|
+
extensions: []
|
89
|
+
extra_rdoc_files: []
|
90
|
+
files:
|
91
|
+
- ".gitignore"
|
92
|
+
- Gemfile
|
93
|
+
- LICENSE.txt
|
94
|
+
- README.md
|
95
|
+
- Rakefile
|
96
|
+
- bin/rake
|
97
|
+
- integrated_data.gemspec
|
98
|
+
- lib/integrated_data.rb
|
99
|
+
- lib/integrated_data/entity.rb
|
100
|
+
- lib/integrated_data/entity/integrator.rb
|
101
|
+
- lib/integrated_data/entity/lookup.rb
|
102
|
+
- lib/integrated_data/identifier.rb
|
103
|
+
- lib/integrated_data/source.rb
|
104
|
+
- test/integrated_data/entity_test.rb
|
105
|
+
- test/integrated_data/identifier_test.rb
|
106
|
+
- test/integrated_data/source_test.rb
|
107
|
+
- test/integrated_data_test.rb
|
108
|
+
- test/test_helper.rb
|
109
|
+
homepage: ''
|
110
|
+
licenses:
|
111
|
+
- MIT
|
112
|
+
metadata: {}
|
113
|
+
post_install_message:
|
114
|
+
rdoc_options: []
|
115
|
+
require_paths:
|
116
|
+
- lib
|
117
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
118
|
+
requirements:
|
119
|
+
- - ">="
|
120
|
+
- !ruby/object:Gem::Version
|
121
|
+
version: '0'
|
122
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
123
|
+
requirements:
|
124
|
+
- - ">="
|
125
|
+
- !ruby/object:Gem::Version
|
126
|
+
version: '0'
|
127
|
+
requirements: []
|
128
|
+
rubyforge_project:
|
129
|
+
rubygems_version: 2.2.2
|
130
|
+
signing_key:
|
131
|
+
specification_version: 4
|
132
|
+
summary: A tool for compositing custom domain objects from disparate data sources.
|
133
|
+
test_files:
|
134
|
+
- test/integrated_data/entity_test.rb
|
135
|
+
- test/integrated_data/identifier_test.rb
|
136
|
+
- test/integrated_data/source_test.rb
|
137
|
+
- test/integrated_data_test.rb
|
138
|
+
- test/test_helper.rb
|