cartograph 1.0.0

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.
@@ -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