kashmir 0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,66 @@
1
+ require 'kashmir/extensions'
2
+
3
+ module Kashmir
4
+ module Caching
5
+ class Memory
6
+
7
+ def from_cache(definitions, instance)
8
+ key = presenter_key(definitions, instance)
9
+ if cached_data = get(key)
10
+ return cached_data
11
+ end
12
+ end
13
+
14
+ def bulk_from_cache(definitions, instances)
15
+ keys = instances.map do |instance|
16
+ presenter_key(definitions, instance) if instance.respond_to?(:id)
17
+ end
18
+
19
+ keys.map do |key|
20
+ get(key)
21
+ end
22
+ end
23
+
24
+ def store_presenter(definitions, representation, instance, ttl=0)
25
+ key = presenter_key(definitions, instance)
26
+ set(key, representation)
27
+ end
28
+
29
+ def bulk_write(definitions, representations, objects, ttl)
30
+ objects.each_with_index do |instance, index|
31
+ store_presenter(definitions, representations[index], instance, ttl)
32
+ end
33
+ end
34
+
35
+ def presenter_key(definition_name, instance)
36
+ "presenter:#{instance.class}:#{instance.id}:#{definition_name}"
37
+ end
38
+
39
+ def get(key)
40
+ @@cache ||= {}
41
+ if data = @@cache[key]
42
+ SymbolizeHelper.symbolize_recursive JSON.parse(data)
43
+ end
44
+ end
45
+
46
+ def set(key, value)
47
+ @@cache ||= {}
48
+ @@cache[key] = value.to_json
49
+ end
50
+
51
+ def clear(definition, instance)
52
+ key = presenter_key(definition, instance)
53
+ @@cache ||= {}
54
+ @@cache.delete(key)
55
+ end
56
+
57
+ def flush!
58
+ @@cache = {}
59
+ end
60
+
61
+ def keys
62
+ @@cache.keys
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,42 @@
1
+ require 'kashmir/extensions'
2
+
3
+ module Kashmir
4
+ module Caching
5
+ class Null
6
+
7
+ def from_cache(definitions, instance)
8
+ nil
9
+ end
10
+
11
+ def bulk_from_cache(definitions, instances)
12
+ []
13
+ end
14
+
15
+ def store_presenter(definitions, representation, instance, black_list=[], ttl=0)
16
+ end
17
+
18
+ def bulk_write(representation_definition, representations, objects, ttl)
19
+ end
20
+
21
+ def presenter_key(definition_name, instance)
22
+ "presenter:#{instance.class}:#{instance.id}:#{definition_name}"
23
+ end
24
+
25
+ def get(key)
26
+ end
27
+
28
+ def set(key, value)
29
+ end
30
+
31
+ def clear(definition, instance)
32
+ end
33
+
34
+ def flush!
35
+ end
36
+
37
+ def keys
38
+ []
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,88 @@
1
+ module Kashmir
2
+ module Representable
3
+
4
+ def represent(representation_definition=[], level=1, skip_cache=false)
5
+ if !skip_cache && cacheable? and cached_presenter = Kashmir::Caching.from_cache(representation_definition, self)
6
+ return cached_presenter
7
+ end
8
+
9
+ representation = {}
10
+
11
+ (representation_definition + base_representation).each do |representation_definition|
12
+ key, arguments = parse_definition(representation_definition)
13
+
14
+ unless self.class.definitions.keys.include?(key)
15
+ raise "#{self.class.to_s}##{key} is not defined as a representation"
16
+ end
17
+
18
+ represented_document = self.class.definitions[key].run_for(self, arguments, level)
19
+ representation = representation.merge(represented_document)
20
+ end
21
+
22
+ if !skip_cache
23
+ cache!(representation_definition.dup, representation.dup, level)
24
+ end
25
+
26
+ representation
27
+ end
28
+
29
+ def cache!(representation_definition, representation, level=1)
30
+ return unless cacheable?
31
+
32
+ (cache_black_list & representation_definition).each do |field_name|
33
+ representation_definition = representation_definition - [ field_name ]
34
+ representation.delete(field_name)
35
+ end
36
+
37
+ Kashmir::Caching.store_presenter(representation_definition, representation, self, level * 60)
38
+ end
39
+
40
+ def cache_black_list
41
+ self.class.definitions.values.reject(&:should_cache?).map(&:field)
42
+ end
43
+
44
+ def cacheable?
45
+ respond_to?(:id)
46
+ end
47
+
48
+ def base_representation
49
+ self.class.definitions.values.select(&:is_base?).map(&:field)
50
+ end
51
+
52
+ def represent_with(&block)
53
+ definitions = Kashmir::InlineDsl.build(&block).definitions
54
+ represent(definitions)
55
+ end
56
+
57
+ def parse_definition(representation_definition)
58
+ if representation_definition.is_a?(Symbol)
59
+ [ representation_definition, [] ]
60
+ elsif representation_definition.is_a?(Hash)
61
+ [ representation_definition.keys.first, representation_definition.values.flatten ]
62
+ end
63
+ end
64
+
65
+ module ClassMethods
66
+
67
+ def representations(&definitions)
68
+ @definitions = {}
69
+ class_eval(&definitions)
70
+ end
71
+
72
+ def base(fields)
73
+ fields.each do |field|
74
+ rep(field, { is_base: true })
75
+ end
76
+ end
77
+
78
+ def rep(field, options={})
79
+ representation = Representation.new(field, options)
80
+ definitions[field] = representation
81
+ end
82
+
83
+ def definitions
84
+ @definitions ||= {}
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,117 @@
1
+ module Kashmir
2
+ class Representation
3
+
4
+ attr_reader :field
5
+
6
+ def initialize(field, options)
7
+ @field = field
8
+ @options = options
9
+ end
10
+
11
+ def run_for(instance, arguments, level=1)
12
+ representation = {}
13
+ instance_vars = instance.instance_variables
14
+
15
+ value = read_value(instance, @field)
16
+ if primitive?(value)
17
+ representation[@field] = value
18
+ else
19
+ if value.is_a?(Hash)
20
+ representation[@field] = new_hash
21
+ else
22
+ representation[@field] = present_value(value, arguments, level)
23
+ end
24
+ end
25
+
26
+ representation
27
+ end
28
+
29
+ def is_base?
30
+ @options.has_key?(:is_base) and !!@options[:is_base]
31
+ end
32
+
33
+ def should_cache?
34
+ if @options.has_key?(:cacheable)
35
+ return !!@options[:cacheable]
36
+ end
37
+
38
+ true
39
+ end
40
+
41
+ def present_value(value, arguments, level=1, skip_cache=false)
42
+
43
+ if value.is_a?(Kashmir)
44
+ return value.represent(arguments, level + 1, skip_cache)
45
+ end
46
+
47
+ if value.is_a?(Hash)
48
+ return present_hash(value, arguments, level + 1, skip_cache)
49
+ end
50
+
51
+ if value.is_a?(Array)
52
+ return present_array(value, arguments, level + 1, skip_cache)
53
+ end
54
+
55
+ if value.respond_to?(:represent)
56
+ return value.represent(arguments, skip_cache)
57
+ end
58
+ end
59
+
60
+ def present_array(value, arguments, level=1, skip_cache=false)
61
+ cached_presenters = Kashmir::Caching.bulk_from_cache(arguments, value)
62
+
63
+ uncached = []
64
+ value.zip(cached_presenters).each do |record, cached_presenter|
65
+ if cached_presenter.nil?
66
+ uncached << record
67
+ end
68
+ end
69
+
70
+ uncached_representations = uncached.map do |element|
71
+ if primitive?(element)
72
+ element
73
+ else
74
+ present_value(element, arguments, level, true)
75
+ end
76
+ end
77
+
78
+ if rep = uncached.first and rep.is_a?(Kashmir) and rep.cacheable?
79
+ Kashmir::Caching.bulk_write(arguments, uncached_representations, uncached, level * 60)
80
+ end
81
+
82
+ cached_presenters.compact + uncached_representations
83
+ end
84
+
85
+ def present_hash(value, arguments, level=1, skip_cache=false)
86
+ new_hash = {}
87
+ value.each_pair do |key, value|
88
+ args = if arguments.is_a?(Hash)
89
+ arguments[key.to_sym]
90
+ else
91
+ arg = arguments.find do |arg|
92
+ (arg.is_a?(Hash) && arg.has_key?(key.to_sym)) || arg == key.to_sym
93
+ end
94
+ if arg.is_a?(Hash)
95
+ arg = arg[key.to_sym]
96
+ end
97
+
98
+ arg
99
+ end
100
+ new_hash[key] = primitive?(value) ? value : present_value(value, args || [], level, skip_cache)
101
+ end
102
+ new_hash
103
+ end
104
+
105
+ def read_value(instance, field)
106
+ if instance.respond_to?(field)
107
+ instance.send(field)
108
+ else
109
+ instance.instance_variable_get("@#{field}")
110
+ end
111
+ end
112
+
113
+ def primitive?(field_value)
114
+ [Fixnum, String, Date, Time, TrueClass, FalseClass, Symbol].include?(field_value.class)
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,3 @@
1
+ module Kashmir
2
+ VERSION = "0.1"
3
+ end
@@ -0,0 +1,127 @@
1
+ require 'ar_test_helper'
2
+
3
+ # see support/ar_models for model definitions
4
+
5
+ describe 'ActiveRecord integration' do
6
+
7
+ before(:each) do
8
+ @tom = TestData.create_tom
9
+ @pastrami_sandwich = AR::Recipe.find_by_title('Pastrami Sandwich')
10
+ @belly_burger = AR::Recipe.find_by_title('Belly Burger')
11
+ @restaurant = AR::Restaurant.find_by_name('Chef Tom Belly Burgers')
12
+ end
13
+
14
+ it 'represents ar objects' do
15
+ ps = @pastrami_sandwich.represent_with do
16
+ prop :title
17
+ end
18
+
19
+ assert_equal ps, { title: 'Pastrami Sandwich' }
20
+ end
21
+
22
+ describe 'ActiveRecord::Relation' do
23
+ it 'represents relations' do
24
+ recipes = AR::Recipe.all.represent_with do
25
+ prop :title
26
+ end
27
+
28
+ assert_equal recipes, [
29
+ { title: 'Pastrami Sandwich' },
30
+ { title: 'Belly Burger' }
31
+ ]
32
+ end
33
+
34
+ it 'represents nested relations' do
35
+ recipes = AR::Recipe.all.represent_with do
36
+ prop :title
37
+ inline :ingredients do
38
+ prop :name
39
+ end
40
+ end
41
+
42
+ assert_equal recipes, [
43
+ {
44
+ title: 'Pastrami Sandwich',
45
+ ingredients: [ { name: 'Pastrami' }, { name: 'Cheese' } ]
46
+ },
47
+ {
48
+ title: 'Belly Burger',
49
+ ingredients: [ {name: 'Pork Belly'}, { name: 'Green Apple' } ]
50
+ }
51
+ ]
52
+ end
53
+ end
54
+
55
+ describe 'belongs_to' do
56
+ it 'works for basic relations' do
57
+ ps = @pastrami_sandwich.represent_with do
58
+ prop :title
59
+ inline :chef do
60
+ prop :name
61
+ end
62
+ end
63
+
64
+ assert_equal ps, {
65
+ title: 'Pastrami Sandwich',
66
+ chef: {
67
+ name: 'Tom'
68
+ }
69
+ }
70
+ end
71
+
72
+ it 'works with custom names' do
73
+ r = @restaurant.represent_with do
74
+ prop :name
75
+ inline :owner do
76
+ prop :name
77
+ end
78
+ end
79
+
80
+ assert_equal r, {
81
+ name: 'Chef Tom Belly Burgers',
82
+ owner: {
83
+ name: 'Tom'
84
+ }
85
+ }
86
+ end
87
+ end
88
+
89
+ describe 'has_many' do
90
+ it 'works for basic associations' do
91
+ t = @tom.represent_with do
92
+ prop :name
93
+ inline :recipes do
94
+ prop :title
95
+ end
96
+ end
97
+
98
+ assert_equal t, {
99
+ name: 'Tom',
100
+ recipes: [
101
+ { title: 'Pastrami Sandwich' },
102
+ { title: 'Belly Burger'}
103
+ ]
104
+ }
105
+ end
106
+
107
+ it 'works with :through associations' do
108
+ tom_with_ingredients = @tom.reload.represent_with do
109
+ prop :name
110
+ inline :ingredients do
111
+ prop :name
112
+ prop :quantity
113
+ end
114
+ end
115
+
116
+ assert_equal tom_with_ingredients, {
117
+ name: 'Tom',
118
+ ingredients: [
119
+ { name: 'Pastrami' , quantity: 'a lot' },
120
+ { name: 'Cheese' , quantity: '1 slice' },
121
+ { name: 'Pork Belly' , quantity: 'plenty' },
122
+ { name: 'Green Apple' , quantity: '2 slices' }
123
+ ]
124
+ }
125
+ end
126
+ end
127
+ end