cartograph 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,24 @@
1
+ require "cartograph/version"
2
+ require 'json'
3
+
4
+ module Cartograph
5
+ autoload :DSL, 'cartograph/dsl'
6
+ autoload :Map, 'cartograph/map'
7
+ autoload :Property, 'cartograph/property'
8
+ autoload :PropertyCollection, 'cartograph/property_collection'
9
+ autoload :RootKey, 'cartograph/root_key'
10
+ autoload :ScopeProxy, 'cartograph/scope_proxy'
11
+
12
+ autoload :Artist, 'cartograph/artist'
13
+ autoload :Sculptor, 'cartograph/sculptor'
14
+
15
+ class << self
16
+ attr_accessor :default_dumper
17
+ attr_accessor :default_loader
18
+ attr_accessor :default_cache
19
+ attr_accessor :default_cache_key
20
+ end
21
+
22
+ self.default_dumper = JSON
23
+ self.default_loader = JSON
24
+ end
@@ -0,0 +1,34 @@
1
+ module Cartograph
2
+ class Artist
3
+ attr_reader :object, :map
4
+
5
+ def initialize(object, map)
6
+ @object = object
7
+ @map = map
8
+ end
9
+
10
+ def properties
11
+ map.properties
12
+ end
13
+
14
+ def draw(scope = nil)
15
+ if map.cache
16
+ cache_key = map.cache_key.call(object, scope)
17
+ map.cache.fetch(cache_key) { build_properties(scope) }
18
+ else
19
+ build_properties(scope)
20
+ end
21
+ end
22
+
23
+ def build_properties(scope)
24
+ scoped_properties = scope ? properties.filter_by_scope(scope) : properties
25
+ scoped_properties.each_with_object({}) do |property, mapped|
26
+ begin
27
+ mapped[property.key] = property.value_for(object, scope)
28
+ rescue NoMethodError => e
29
+ raise ArgumentError, "#{object} does not respond to #{property.name}, so we can't map it"
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,104 @@
1
+ module Cartograph
2
+ module DSL
3
+ def self.included(base)
4
+ base.extend ClassMethods
5
+ end
6
+
7
+ module ClassMethods
8
+ def cartograph(&block)
9
+ @cartograph_map ||= Map.new
10
+
11
+ if block_given?
12
+ block.arity > 0 ? block.call(@cartograph_map) : @cartograph_map.instance_eval(&block)
13
+ end
14
+
15
+ @cartograph_map
16
+ end
17
+
18
+ # Returns a hash representation of the object based on the mapping
19
+ #
20
+ # @param scope [Symbol] the scope of the mapping
21
+ # @param object the object to be mapped
22
+ # @return [Hash, Array]
23
+ def hash_for(scope, object)
24
+ drawn_object = Artist.new(object, @cartograph_map).draw(scope)
25
+ prepend_root_key(scope, :singular, drawn_object)
26
+ end
27
+
28
+ # Returns a hash representation of the collection of objects based on the mapping
29
+ #
30
+ # @param scope [Symbol] the scope of the mapping
31
+ # @params objects [Array] the array of objects to be mapped
32
+ # @return [Hash, Array]
33
+ def hash_collection_for(scope, objects)
34
+ drawn_objects = objects.map do |object|
35
+ Artist.new(object, @cartograph_map).draw(scope)
36
+ end
37
+
38
+ prepend_root_key(scope, :plural, drawn_objects)
39
+ end
40
+
41
+ def representation_for(scope, object, dumper = Cartograph.default_dumper)
42
+ dumper.dump(hash_for(scope, object))
43
+ end
44
+
45
+ def represent_collection_for(scope, objects, dumper = Cartograph.default_dumper)
46
+ dumper.dump(hash_collection_for(scope, objects))
47
+ end
48
+
49
+ def extract_single(content, scope, loader = Cartograph.default_loader)
50
+ loaded = loader.load(content)
51
+
52
+ retrieve_root_key(scope, :singular) do |root_key|
53
+ # Reassign loaded if a root key exists
54
+ loaded = loaded[root_key]
55
+ end
56
+
57
+ Sculptor.new(loaded, @cartograph_map).sculpt(scope)
58
+ end
59
+
60
+ def extract_into_object(object, content, scope, loader = Cartograph.default_loader)
61
+ loaded = loader.load(content)
62
+
63
+ retrieve_root_key(scope, :singular) do |root_key|
64
+ # Reassign loaded if a root key exists
65
+ loaded = loaded[root_key]
66
+ end
67
+
68
+ sculptor = Sculptor.new(loaded, @cartograph_map)
69
+ sculptor.sculpted_object = object
70
+ sculptor.sculpt(scope)
71
+ end
72
+
73
+ def extract_collection(content, scope, loader = Cartograph.default_loader)
74
+ loaded = loader.load(content)
75
+
76
+ retrieve_root_key(scope, :plural) do |root_key|
77
+ # Reassign loaded if a root key exists
78
+ loaded = loaded[root_key]
79
+ end
80
+
81
+ loaded.map do |object|
82
+ Sculptor.new(object, @cartograph_map).sculpt(scope)
83
+ end
84
+ end
85
+
86
+ private
87
+ def prepend_root_key(scope, plurality, payload)
88
+ retrieve_root_key(scope, plurality) do |root_key|
89
+ # Reassign drawed if a root key exists
90
+ payload = { root_key => payload }
91
+ end
92
+
93
+ payload
94
+ end
95
+
96
+ def retrieve_root_key(scope, type, &block)
97
+ if root_key = @cartograph_map.root_key_for(scope, type)
98
+ yield root_key
99
+ end
100
+ end
101
+
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,91 @@
1
+ require 'thread'
2
+
3
+ module Cartograph
4
+ class Map
5
+ def initialize
6
+ @scope_mutex = Mutex.new
7
+ end
8
+
9
+ def property(*args, &block)
10
+ options = args.last.is_a?(Hash) ? args.pop : {}
11
+
12
+ # Append scopes if we're currently mapping in a scoped block
13
+ options[:scopes] ||= []
14
+ options[:scopes] += Array(@current_scopes)
15
+
16
+ args.each do |prop|
17
+ properties << Property.new(prop, options, &block)
18
+ end
19
+ end
20
+
21
+ def properties
22
+ @properties ||= PropertyCollection.new
23
+ end
24
+
25
+ def scoped(*scopes, &block)
26
+ @scope_mutex.synchronize do
27
+ @current_scopes = scopes
28
+
29
+ instance_eval(&block) if block_given?
30
+
31
+ @current_scopes = nil
32
+ end
33
+ end
34
+
35
+ def root_keys
36
+ @root_keys ||= []
37
+ end
38
+
39
+ def mapping(klass = nil)
40
+ @mapping = klass if klass
41
+ @mapping
42
+ end
43
+
44
+ def root_key(options)
45
+ root_keys << RootKey.new(options)
46
+ end
47
+
48
+ def cache(object = nil)
49
+ @cache = object unless object.nil?
50
+ @cache.nil? ? Cartograph.default_cache : @cache
51
+ end
52
+
53
+ def cache_key(&calculator)
54
+ @cache_calculator = calculator if block_given?
55
+ @cache_calculator.nil? ? Cartograph.default_cache_key : @cache_calculator
56
+ end
57
+
58
+ def root_key_for(scope, type)
59
+ return unless %i(singular plural).include?(type)
60
+
61
+ if (root_key = root_keys.select {|rk| rk.scopes.include?(scope) }[0])
62
+ root_key.send(type)
63
+ end
64
+ end
65
+
66
+ def dup
67
+ Cartograph::Map.new.tap do |map|
68
+ self.properties.each do |property|
69
+ map.properties << property.dup
70
+ end
71
+
72
+ map.mapping self.mapping
73
+
74
+ self.root_keys.each do |rk|
75
+ map.root_keys << rk
76
+ end
77
+
78
+ map.cache self.cache
79
+ map.cache_key &self.cache_key if self.cache_key
80
+ end
81
+ end
82
+
83
+ def ==(other)
84
+ methods = %i(properties root_keys mapping cache cache_key)
85
+ methods.inject(true) do |current_value, method|
86
+ break unless current_value
87
+ send(method) == other.send(method)
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,69 @@
1
+ module Cartograph
2
+ class Property
3
+ attr_reader :name, :options
4
+ attr_accessor :map
5
+
6
+ def initialize(name, options = {}, &block)
7
+ @name = name
8
+ @options = options
9
+
10
+ if mapped_class = options[:include]
11
+ # Perform a safe duplication into our properties map
12
+ # This allows the user to define more attributes on the map should they need to
13
+ @map = mapped_class.cartograph.dup
14
+ end
15
+
16
+ if block_given?
17
+ @map ||= Map.new
18
+ block.arity > 0 ? block.call(map) : map.instance_eval(&block)
19
+ end
20
+ end
21
+
22
+ def key
23
+ (options[:key] || name).to_s
24
+ end
25
+
26
+ def value_for(object, scope = nil)
27
+ value = object.send(name)
28
+ return if value.nil?
29
+ map ? artist_value(value, scope) : value
30
+ end
31
+
32
+ def value_from(object, scope = nil)
33
+ return if object.nil?
34
+ value = object.has_key?(key) ? object[key] : object[key.to_sym]
35
+ map ? sculpt_value(value, scope) : value
36
+ end
37
+
38
+ def scopes
39
+ Array(options[:scopes] || [])
40
+ end
41
+
42
+ def plural?
43
+ !!options[:plural]
44
+ end
45
+
46
+ def dup
47
+ Property.new(name, options.dup).tap do |property|
48
+ property.map = map.dup if self.map
49
+ end
50
+ end
51
+
52
+ def ==(other)
53
+ %i(name options map).inject(true) do |equals, method|
54
+ break unless equals
55
+ send(method) == other.send(method)
56
+ end
57
+ end
58
+
59
+ private
60
+
61
+ def sculpt_value(value, scope)
62
+ plural? ? Array(value).map {|v| Sculptor.new(v, map).sculpt(scope) } : Sculptor.new(value, map).sculpt(scope)
63
+ end
64
+
65
+ def artist_value(value, scope)
66
+ plural? ? Array(value).map {|v| Artist.new(v, map).draw(scope) } : Artist.new(value, map).draw(scope)
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,27 @@
1
+ require 'forwardable'
2
+
3
+ module Cartograph
4
+ class PropertyCollection
5
+ # Make this collection quack like an array
6
+ # http://words.steveklabnik.com/beware-subclassing-ruby-core-classes
7
+ extend Forwardable
8
+ def_delegators :@collection, *(Array.instance_methods - Object.instance_methods)
9
+
10
+ def initialize(*)
11
+ @collection = []
12
+ end
13
+
14
+ def filter_by_scope(scope)
15
+ select do |property|
16
+ property.scopes.include?(scope)
17
+ end
18
+ end
19
+
20
+ def ==(other)
21
+ each_with_index.inject(true) do |current_value, (property, index)|
22
+ break unless current_value
23
+ property == other[index]
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,19 @@
1
+ module Cartograph
2
+ class RootKey
3
+ attr_reader :options
4
+
5
+ def initialize(options = {})
6
+ @options = options
7
+ end
8
+
9
+ def scopes
10
+ Array(options[:scopes]) || []
11
+ end
12
+
13
+ %i(singular plural).each do |method|
14
+ define_method(method) do
15
+ options[method]
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,40 @@
1
+ module Cartograph
2
+ class Sculptor
3
+ attr_reader :object, :map
4
+
5
+ def initialize(object, map)
6
+ @object = object
7
+ @map = map
8
+ end
9
+
10
+ def properties
11
+ map.properties
12
+ end
13
+
14
+ # Set this to pass in an object to extract into. Must
15
+ # @param object must be of the same class as the map#mapping
16
+ def sculpted_object=(object)
17
+ raise ArgumentError unless object.is_a?(map.mapping)
18
+
19
+ @sculpted_object = object
20
+ end
21
+
22
+ def sculpt(scope = nil)
23
+ return unless @object
24
+
25
+ scoped_properties = scope ? properties.filter_by_scope(scope) : properties
26
+
27
+ attributes = scoped_properties.each_with_object({}) do |property, h|
28
+ h[property.name] = property.value_from(object, scope)
29
+ end
30
+
31
+ return map.mapping.new(attributes) unless @sculpted_object
32
+
33
+ attributes.each do |name, val|
34
+ @sculpted_object[name] = val
35
+ end
36
+
37
+ @sculpted_object
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,3 @@
1
+ module Cartograph
2
+ VERSION = '1.0.0'
3
+ end
@@ -0,0 +1,115 @@
1
+ require 'spec_helper'
2
+
3
+ describe Cartograph::Artist do
4
+ let(:map) { Cartograph::Map.new }
5
+ let(:properties) { map.properties }
6
+
7
+ describe '#initialize' do
8
+ it 'initializes with an object and a map' do
9
+ object = double('object', name: 'hello')
10
+ properties << Cartograph::Property.new(:name)
11
+
12
+ artist = Cartograph::Artist.new(object, map)
13
+
14
+ expect(artist.object).to be(object)
15
+ expect(artist.map).to be(map)
16
+ end
17
+ end
18
+
19
+ describe '#draw' do
20
+ it 'returns a hash of mapped properties' do
21
+ object = double('object', hello: 'world')
22
+ properties << Cartograph::Property.new(:hello)
23
+
24
+ artist = Cartograph::Artist.new(object, map)
25
+ masterpiece = artist.draw
26
+
27
+ expect(masterpiece).to include('hello' => 'world')
28
+ end
29
+
30
+ it 'raises for a property that the object does not have' do
31
+ class TestArtistNoMethod; end
32
+ object = TestArtistNoMethod.new
33
+ properties << Cartograph::Property.new(:bunk)
34
+ artist = Cartograph::Artist.new(object, map)
35
+
36
+ expect { artist.draw }.to raise_error(ArgumentError).with_message("#{object} does not respond to bunk, so we can't map it")
37
+ end
38
+
39
+ context 'for a property with a key set on it' do
40
+ it 'returns the hash with the key set correctly' do
41
+ object = double('object', hello: 'world')
42
+ properties << Cartograph::Property.new(:hello, key: :hola)
43
+
44
+ artist = Cartograph::Artist.new(object, map)
45
+ masterpiece = artist.draw
46
+
47
+ expect(masterpiece).to include('hola' => 'world')
48
+ end
49
+ end
50
+
51
+ context 'for filtered drawing' do
52
+ it 'only returns the scoped properties' do
53
+ object = double('object', hello: 'world', foo: 'bar')
54
+ properties << Cartograph::Property.new(:hello, scopes: [:create, :read])
55
+ properties << Cartograph::Property.new(:foo, scopes: [:create])
56
+
57
+ artist = Cartograph::Artist.new(object, map)
58
+ masterpiece = artist.draw(:read)
59
+
60
+ expect(masterpiece).to eq('hello' => 'world')
61
+ end
62
+
63
+ context 'on nested properties' do
64
+ it 'only returns the nested properties within the same scope' do
65
+ child = double('child', hello: 'world', foo: 'bunk')
66
+ object = double('object', child: child)
67
+
68
+ root_property = Cartograph::Property.new(:child, scopes: [:create, :read]) do
69
+ property :hello, scopes: [:create]
70
+ property :foo, scopes: [:read]
71
+ end
72
+
73
+ properties << root_property
74
+
75
+ artist = Cartograph::Artist.new(object, map)
76
+ masterpiece = artist.draw(:read)
77
+
78
+ expect(masterpiece).to eq('child' => { 'foo' => child.foo })
79
+ end
80
+ end
81
+ end
82
+
83
+ context "with caching enabled" do
84
+ let(:cacher) { double('cacher', fetch: { foo: 'cached-value' }) }
85
+ let(:object) { double('object', foo: 'bar', cache_key: 'test-cache-key') }
86
+
87
+ it "uses the cache fetch for values" do
88
+ map.cache(cacher)
89
+ map.cache_key { |obj, scope| obj.cache_key }
90
+ map.property :foo
91
+
92
+ artist = Cartograph::Artist.new(object, map)
93
+ masterpiece = artist.draw
94
+
95
+ expect(masterpiece).to eq(cacher.fetch)
96
+
97
+ expect(cacher).to have_received(:fetch).with('test-cache-key')
98
+ end
99
+
100
+ it "uses the cache key for the object and scope" do
101
+ called = double(call: 'my-cache')
102
+
103
+ map.cache(cacher)
104
+ map.cache_key { |obj, scope| called.call(obj, scope) }
105
+
106
+ map.property :foo, scopes: [:read]
107
+
108
+ artist = Cartograph::Artist.new(object, map)
109
+ masterpiece = artist.draw(:read)
110
+
111
+ expect(called).to have_received(:call).with(object, :read)
112
+ end
113
+ end
114
+ end
115
+ end