ensconce 0.0.2
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 +15 -0
- data/MIT-LICENSE +20 -0
- data/README.rdoc +69 -0
- data/Rakefile +9 -0
- data/lib/ensconce.rb +10 -0
- data/lib/ensconce/adapters/adapter.rb +52 -0
- data/lib/ensconce/adapters/mydex_adapter.rb +75 -0
- data/lib/ensconce/adapters/yaml_file_adapter.rb +21 -0
- data/lib/ensconce/data_store.rb +41 -0
- data/lib/ensconce/hash_builder.rb +72 -0
- data/lib/ensconce/key_mappers/key_map.rb +35 -0
- data/lib/ensconce/key_mappers/mydex_key_map.rb +23 -0
- data/lib/ensconce/mangle.rb +31 -0
- data/lib/ensconce/version.rb +15 -0
- data/test/data/users.yml +9 -0
- data/test/ensconce/adapters/adapter_test.rb +22 -0
- data/test/ensconce/adapters/mydex_adapter_test.rb +49 -0
- data/test/ensconce/adapters/yaml_file_adapter_test.rb +31 -0
- data/test/ensconce/data_store_test.rb +72 -0
- data/test/ensconce/key_mappers/hash_builder_test.rb +102 -0
- data/test/ensconce/key_mappers/mydex_key_map_test.rb +20 -0
- data/test/ensconce/mangle_test.rb +25 -0
- data/test/fixtures/users.yml +9 -0
- data/test/fixtures/vcr_cassettes/datastore_after_mydex_adapter_test.yml +44 -0
- data/test/fixtures/vcr_cassettes/datastore_before_mydex_adapter_test.yml +85 -0
- data/test/fixtures/vcr_cassettes/datastore_get_Harry.yml +44 -0
- data/test/fixtures/vcr_cassettes/datastore_get_Mary.yml +44 -0
- data/test/fixtures/vcr_cassettes/datastore_get_Trevor.yml +44 -0
- data/test/fixtures/vcr_cassettes/datastore_push_Harry.yml +44 -0
- data/test/fixtures/vcr_cassettes/datastore_push_Mary.yml +44 -0
- data/test/fixtures/vcr_cassettes/datastore_push_Trevor.yml +44 -0
- data/test/fixtures/vcr_cassettes/datastore_test_get.yml +44 -0
- data/test/test_helper.rb +67 -0
- metadata +110 -0
checksums.yaml
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
---
|
2
|
+
!binary "U0hBMQ==":
|
3
|
+
metadata.gz: !binary |-
|
4
|
+
NWI1MTk3MjlkYzg1ODNmMGFlMWFmNzUwODA0MjVmZWVjNjlhMWJjMQ==
|
5
|
+
data.tar.gz: !binary |-
|
6
|
+
MTQwODNjZTU5OThlOWI4OTY0MzAwMjMyNmNjZTNjMmE0OTlmMTIwOQ==
|
7
|
+
SHA512:
|
8
|
+
metadata.gz: !binary |-
|
9
|
+
MjJkYTU3NzkzOTU2ODRkN2U0NDc0YzM0MmQyM2MyOGY3OWIzOThmOWNmOTc5
|
10
|
+
MWI4ZjE4MGQwZjMwODE2OTk5MjRhNzhjZGY3NGJkZGViZGMyNWMyNWZjNTdj
|
11
|
+
ZTQyMDc0OTk2ZWY0NzRiMWVhNjE5NmVjNDBjOTg5ZTk0M2NjMGU=
|
12
|
+
data.tar.gz: !binary |-
|
13
|
+
OGU3ZjY0NjU2Y2NjNWViN2UwOGQ2NWY5MWUzZWExZGYxMTVhOGJjYzg5ZGY0
|
14
|
+
MDI2N2M4NmJjOGI3MGM0NmEwNzVlM2Q1NDVhNzgxMDM5NWY4NmY0MDM4ZjE5
|
15
|
+
MDEzY2U0NjVmZTY1YzhlZjVmYjVhNGFlODNhMzIzODMzOTkyNjU=
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2013 Rob Nichols
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
== ensconce
|
2
|
+
|
3
|
+
Ensconce provides a common interface to data stores. The connection between
|
4
|
+
Ensconce::DataStore and a data store provider is configured via an adapter
|
5
|
+
|
6
|
+
=== YAML file data store
|
7
|
+
|
8
|
+
user = User.new(:id => 'user_1')
|
9
|
+
DataStore.adapter = YamlFileAdapter.config(:file => 'path/to/file.yml')
|
10
|
+
data_store = DataStore.open user
|
11
|
+
|
12
|
+
Opens a data store from the file specified within the adapter config. The store
|
13
|
+
could look something like this:
|
14
|
+
|
15
|
+
user_1:
|
16
|
+
some_field: x
|
17
|
+
|
18
|
+
We can get and set 'some_field' like this
|
19
|
+
|
20
|
+
data_store['some_field'] == 'x' # returns true
|
21
|
+
data_store['some_field'] = 'y'
|
22
|
+
data_store.save
|
23
|
+
|
24
|
+
The file will then look like this:
|
25
|
+
|
26
|
+
user_1:
|
27
|
+
some_field: y
|
28
|
+
|
29
|
+
=== Mydex data source
|
30
|
+
|
31
|
+
DataStore.adapter = MydexAdapter.config(
|
32
|
+
url: <mydex url>
|
33
|
+
api_key: <your api key>
|
34
|
+
)
|
35
|
+
|
36
|
+
user = MydexUser.new(
|
37
|
+
key: <from your user's connection settings>
|
38
|
+
con_id: <from your user's connection settings>
|
39
|
+
id: <from your user's connection settings>
|
40
|
+
)
|
41
|
+
|
42
|
+
We can then get the mydex user's data like this:
|
43
|
+
|
44
|
+
data_store = DataStore.open(user, :data_set => 'field_ds_personal_details')
|
45
|
+
data_store['first_name'] --> Mydex user's first name
|
46
|
+
|
47
|
+
Updating data works just as it does for the YAML example:
|
48
|
+
|
49
|
+
data_store['first_name'] = 'Robert'
|
50
|
+
data_store.save
|
51
|
+
|
52
|
+
This will write a new first name to the Mydex user's personal_details.
|
53
|
+
|
54
|
+
=== Key mappers
|
55
|
+
Note that in Mydex the first name field is actually 'field_personal_fname'.
|
56
|
+
Ensconce provides key mappers that allow you to use more generic names for
|
57
|
+
fields, thereby making it easier to swap data stores.
|
58
|
+
|
59
|
+
== Testing ensconce
|
60
|
+
|
61
|
+
To test this applications mydex connection, a settings.yml needs to be added at
|
62
|
+
the root of the ensconce code. This file should have this format:
|
63
|
+
|
64
|
+
mydex:
|
65
|
+
url: 'https://sbx-api.mydex.org/'
|
66
|
+
key: <key from sandbox connection>
|
67
|
+
api_key: <your mydex api key>
|
68
|
+
con_id: <con_id from sandbox connection>
|
69
|
+
id: id <from sandbox connection>
|
data/Rakefile
ADDED
data/lib/ensconce.rb
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
module Ensconce
|
2
|
+
|
3
|
+
# Parent class for adapters.
|
4
|
+
#
|
5
|
+
# Specific adapters should inherit from this class
|
6
|
+
class Adapter
|
7
|
+
attr_reader :settings, :params
|
8
|
+
|
9
|
+
def initialize(args = {})
|
10
|
+
@settings = args[:settings]
|
11
|
+
@params = args[:params]
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.config(options = {})
|
15
|
+
@options = options
|
16
|
+
return self
|
17
|
+
end
|
18
|
+
|
19
|
+
# The object passed to for should have methods that return the settings
|
20
|
+
# for each instance connection. For example, a user object with an id used
|
21
|
+
# to retrieve data for that user.
|
22
|
+
def self.for(settings_object, params = {})
|
23
|
+
new(:settings => settings_object, :params => params)
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.options
|
27
|
+
@options || {}
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.options=(data)
|
31
|
+
@options = data
|
32
|
+
end
|
33
|
+
|
34
|
+
def options
|
35
|
+
self.class.options
|
36
|
+
end
|
37
|
+
|
38
|
+
def get(*args)
|
39
|
+
raise_define_method_error('get')
|
40
|
+
end
|
41
|
+
|
42
|
+
def push(*args)
|
43
|
+
raise_define_method_error('push')
|
44
|
+
end
|
45
|
+
|
46
|
+
|
47
|
+
private
|
48
|
+
def raise_define_method_error(name)
|
49
|
+
raise "Adapter instance method '#{name}' needs to be defined"
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
require 'faraday'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
module Ensconce
|
5
|
+
|
6
|
+
class MydexAdapter < Adapter
|
7
|
+
|
8
|
+
def get
|
9
|
+
@data_set = params[:data_set]
|
10
|
+
response = connection.get(
|
11
|
+
path,
|
12
|
+
params.merge({ dataset: @data_set })
|
13
|
+
)
|
14
|
+
@data = JSON.parse response.body
|
15
|
+
@data = @data[@data_set]['instance_0']
|
16
|
+
change_data_keys_to_data_store_names
|
17
|
+
extact_values
|
18
|
+
end
|
19
|
+
|
20
|
+
def push(data)
|
21
|
+
@data_set = params[:data_set]
|
22
|
+
@data = data
|
23
|
+
change_data_keys_to_mydex_names
|
24
|
+
response = connection.put do |req|
|
25
|
+
req.url path
|
26
|
+
req.headers['Content-Type'] = 'application/json'
|
27
|
+
req.body = {@data_set => [@data]}.to_json
|
28
|
+
req.params = params
|
29
|
+
end
|
30
|
+
response.body
|
31
|
+
end
|
32
|
+
|
33
|
+
def connection
|
34
|
+
Faraday.new(:url => options[:url]) do |faraday|
|
35
|
+
faraday.request :url_encoded # form-encode POST params
|
36
|
+
# faraday.response :logger # log requests to STDOUT
|
37
|
+
faraday.adapter Faraday.default_adapter # make requests with Net::HTTP
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def params
|
42
|
+
super.merge(
|
43
|
+
key: settings.key,
|
44
|
+
api_key: options[:api_key],
|
45
|
+
con_id: settings.con_id,
|
46
|
+
source_type: 'connection'
|
47
|
+
)
|
48
|
+
end
|
49
|
+
|
50
|
+
def path
|
51
|
+
"api/pds/pds/#{settings.id}.json"
|
52
|
+
end
|
53
|
+
|
54
|
+
def change_data_keys_to_data_store_names
|
55
|
+
@data = Mangle.rekey(@data, key_map(@data_set))
|
56
|
+
end
|
57
|
+
|
58
|
+
def change_data_keys_to_mydex_names
|
59
|
+
@data = Mangle.rekey(@data, key_map(@data_set).invert)
|
60
|
+
end
|
61
|
+
|
62
|
+
def extact_values
|
63
|
+
HashBuilder.new(
|
64
|
+
keys: @data.keys,
|
65
|
+
values: @data.values,
|
66
|
+
values_mod: lambda {|value| value['value']}
|
67
|
+
).hash
|
68
|
+
end
|
69
|
+
|
70
|
+
def key_map(key)
|
71
|
+
MydexKeyMap.for(key)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Ensconce
|
2
|
+
class YamlFileAdapter < Adapter
|
3
|
+
|
4
|
+
def self.all
|
5
|
+
YAML.load_file options[:file]
|
6
|
+
end
|
7
|
+
|
8
|
+
def get
|
9
|
+
raise "No file defined" unless options[:file]
|
10
|
+
data = YAML.load_file options[:file]
|
11
|
+
data[settings.id]
|
12
|
+
end
|
13
|
+
|
14
|
+
def push(data)
|
15
|
+
result = Mangle.deep_merge self.class.all, {settings.id => data}
|
16
|
+
File.open(options[:file], 'w+') {|f| f.write(result.to_yaml) }
|
17
|
+
end
|
18
|
+
|
19
|
+
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
|
2
|
+
class DataStore < Hash
|
3
|
+
attr_accessor :settings, :params
|
4
|
+
|
5
|
+
def initialize(settings_object, params = {})
|
6
|
+
super()
|
7
|
+
data = params.delete(:data)
|
8
|
+
@params = params
|
9
|
+
@settings = settings_object
|
10
|
+
replace data if data
|
11
|
+
end
|
12
|
+
|
13
|
+
def save
|
14
|
+
raise "adapter must be specifed" unless adapter
|
15
|
+
adapter.push({}.merge(self))
|
16
|
+
end
|
17
|
+
|
18
|
+
def adapter
|
19
|
+
@adapter ||= self.class.adapter.for(settings, params)
|
20
|
+
end
|
21
|
+
|
22
|
+
def get
|
23
|
+
replace adapter.get.merge(self)
|
24
|
+
end
|
25
|
+
|
26
|
+
|
27
|
+
def self.open(settings_object, params = {})
|
28
|
+
data_store = new settings_object, params
|
29
|
+
data_store.get
|
30
|
+
return data_store
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.adapter=(klass)
|
34
|
+
@adapter_klass = klass
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.adapter
|
38
|
+
@adapter_klass
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
module Ensconce
|
2
|
+
|
3
|
+
# Used to convert pairs of arrays into a hash.
|
4
|
+
#
|
5
|
+
# hash_builder = HashBuilder.new :keys => ['a', 'b'], :values => ['1', '2']
|
6
|
+
# hash_builder.hash --> {'a' => '1', 'b' => '2'}
|
7
|
+
#
|
8
|
+
# Also allows modification of keys or values
|
9
|
+
#
|
10
|
+
# hash_builder.keys_mod = lambda {|key| key.upcase}
|
11
|
+
# hash_builder.hash --> {'A' => '1', 'B' => '2'}
|
12
|
+
#
|
13
|
+
# hash_builder.values_mod = lambda {|value| (value.to_i * 4).to_s}
|
14
|
+
# hash_builder.hash --> {'A' => '4', 'B' => '8'}
|
15
|
+
#
|
16
|
+
# You can use a Proc to define a mod, but I'd recommend not doing so as a
|
17
|
+
# return statement in the Proc can cause an unexpected result (see tests).
|
18
|
+
#
|
19
|
+
class HashBuilder
|
20
|
+
|
21
|
+
attr_accessor :keys, :values, :keys_mod, :values_mod
|
22
|
+
|
23
|
+
def initialize(args = {})
|
24
|
+
@keys = args[:keys]
|
25
|
+
@values = args[:values]
|
26
|
+
@keys_mod = args[:keys_mod]
|
27
|
+
@values_mod = args[:values_mod]
|
28
|
+
end
|
29
|
+
|
30
|
+
def valid?
|
31
|
+
check_required_attributes_present
|
32
|
+
check_mods_are_valid
|
33
|
+
end
|
34
|
+
|
35
|
+
def hash
|
36
|
+
valid?
|
37
|
+
map = [processed_keys, processed_values].transpose
|
38
|
+
Hash[map]
|
39
|
+
end
|
40
|
+
|
41
|
+
def check_mods_are_valid
|
42
|
+
mod_attributes.each do |mod|
|
43
|
+
mod = send mod
|
44
|
+
next unless mod
|
45
|
+
raise ":#{mod} must be a Proc or lambda" unless mod.kind_of? Proc
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def check_required_attributes_present
|
50
|
+
required_attibutes.each do |attribute|
|
51
|
+
raise ":#{attribute} is a required attribute but was not found" unless send(attribute)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def required_attibutes
|
56
|
+
[:keys, :values]
|
57
|
+
end
|
58
|
+
|
59
|
+
def mod_attributes
|
60
|
+
[:keys_mod, :values_mod]
|
61
|
+
end
|
62
|
+
|
63
|
+
def processed_keys
|
64
|
+
keys_mod ? keys.collect(&keys_mod) : keys
|
65
|
+
end
|
66
|
+
|
67
|
+
def processed_values
|
68
|
+
values_mod ? values.collect(&values_mod) : values
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Ensconce
|
2
|
+
|
3
|
+
# Parent class for key maps.
|
4
|
+
#
|
5
|
+
# Specific key maps should inherit from this class
|
6
|
+
class KeyMap
|
7
|
+
|
8
|
+
def self.for(key)
|
9
|
+
key_map = new
|
10
|
+
key_map[key]
|
11
|
+
end
|
12
|
+
|
13
|
+
def mappings
|
14
|
+
@mappings ||= default_mappings
|
15
|
+
end
|
16
|
+
|
17
|
+
def [](key)
|
18
|
+
mappings[key]
|
19
|
+
end
|
20
|
+
|
21
|
+
def map_generator(args)
|
22
|
+
HashBuilder.new(
|
23
|
+
:keys => (args[:original] || args[:keys]),
|
24
|
+
:values => (args[:replacement] || args[:values]),
|
25
|
+
:keys_mod => (args[:original_mod] || args[:keys_mod]),
|
26
|
+
:values_mod => (args[:replacement_mod] || args[:values_mod])
|
27
|
+
).hash
|
28
|
+
end
|
29
|
+
|
30
|
+
def default_mapping
|
31
|
+
raise "default_mapping must be defined"
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Ensconce
|
2
|
+
class MydexKeyMap < KeyMap
|
3
|
+
|
4
|
+
def default_mappings
|
5
|
+
{
|
6
|
+
'field_ds_personal_details' => field_ds_personal_details
|
7
|
+
}
|
8
|
+
end
|
9
|
+
|
10
|
+
def field_ds_personal_details
|
11
|
+
map_generator( self.class.field_ds_personal_details_map )
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.field_ds_personal_details_map
|
15
|
+
{
|
16
|
+
:original => %w{fname faname gender maname mname nickname suffix title},
|
17
|
+
:replacement => %w{first_name last_name gender maiden_name middle_name nick_name suffix title},
|
18
|
+
:original_mod => lambda {|field| "field_personal_#{field}"}
|
19
|
+
}
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
end
|