kashmir 0.1

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,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