ensconce 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
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