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.
Files changed (34) hide show
  1. checksums.yaml +15 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.rdoc +69 -0
  4. data/Rakefile +9 -0
  5. data/lib/ensconce.rb +10 -0
  6. data/lib/ensconce/adapters/adapter.rb +52 -0
  7. data/lib/ensconce/adapters/mydex_adapter.rb +75 -0
  8. data/lib/ensconce/adapters/yaml_file_adapter.rb +21 -0
  9. data/lib/ensconce/data_store.rb +41 -0
  10. data/lib/ensconce/hash_builder.rb +72 -0
  11. data/lib/ensconce/key_mappers/key_map.rb +35 -0
  12. data/lib/ensconce/key_mappers/mydex_key_map.rb +23 -0
  13. data/lib/ensconce/mangle.rb +31 -0
  14. data/lib/ensconce/version.rb +15 -0
  15. data/test/data/users.yml +9 -0
  16. data/test/ensconce/adapters/adapter_test.rb +22 -0
  17. data/test/ensconce/adapters/mydex_adapter_test.rb +49 -0
  18. data/test/ensconce/adapters/yaml_file_adapter_test.rb +31 -0
  19. data/test/ensconce/data_store_test.rb +72 -0
  20. data/test/ensconce/key_mappers/hash_builder_test.rb +102 -0
  21. data/test/ensconce/key_mappers/mydex_key_map_test.rb +20 -0
  22. data/test/ensconce/mangle_test.rb +25 -0
  23. data/test/fixtures/users.yml +9 -0
  24. data/test/fixtures/vcr_cassettes/datastore_after_mydex_adapter_test.yml +44 -0
  25. data/test/fixtures/vcr_cassettes/datastore_before_mydex_adapter_test.yml +85 -0
  26. data/test/fixtures/vcr_cassettes/datastore_get_Harry.yml +44 -0
  27. data/test/fixtures/vcr_cassettes/datastore_get_Mary.yml +44 -0
  28. data/test/fixtures/vcr_cassettes/datastore_get_Trevor.yml +44 -0
  29. data/test/fixtures/vcr_cassettes/datastore_push_Harry.yml +44 -0
  30. data/test/fixtures/vcr_cassettes/datastore_push_Mary.yml +44 -0
  31. data/test/fixtures/vcr_cassettes/datastore_push_Trevor.yml +44 -0
  32. data/test/fixtures/vcr_cassettes/datastore_test_get.yml +44 -0
  33. data/test/test_helper.rb +67 -0
  34. metadata +110 -0
@@ -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=
@@ -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.
@@ -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>
@@ -0,0 +1,9 @@
1
+
2
+ require 'rubygems'
3
+ require 'rake'
4
+ require 'rake/clean'
5
+ require 'rake/testtask'
6
+
7
+ Rake::TestTask.new do |t|
8
+ t.test_files = FileList['test/**/*.rb']
9
+ end
@@ -0,0 +1,10 @@
1
+ # load order specific files
2
+ require_relative 'ensconce/adapters/adapter'
3
+ require_relative 'ensconce/key_mappers/key_map'
4
+
5
+ # load everything else
6
+ Dir[File.dirname(__FILE__) + "/ensconce/**/*.rb"].each{|file| require file}
7
+
8
+ module Ensconce
9
+
10
+ end
@@ -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