kashmir 0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +49 -0
- data/LICENSE.txt +22 -0
- data/README.md +563 -0
- data/Rakefile +11 -0
- data/kashmir.gemspec +35 -0
- data/lib/kashmir.rb +42 -0
- data/lib/kashmir/caching.rb +50 -0
- data/lib/kashmir/dsl.rb +41 -0
- data/lib/kashmir/extensions.rb +18 -0
- data/lib/kashmir/inline_dsl.rb +13 -0
- data/lib/kashmir/patches/active_record.rb +68 -0
- data/lib/kashmir/plugins/active_record_representation.rb +14 -0
- data/lib/kashmir/plugins/ar.rb +49 -0
- data/lib/kashmir/plugins/ar_relation.rb +35 -0
- data/lib/kashmir/plugins/memcached_caching.rb +83 -0
- data/lib/kashmir/plugins/memory_caching.rb +66 -0
- data/lib/kashmir/plugins/null_caching.rb +42 -0
- data/lib/kashmir/representable.rb +88 -0
- data/lib/kashmir/representation.rb +117 -0
- data/lib/kashmir/version.rb +3 -0
- data/test/activerecord_test.rb +127 -0
- data/test/activerecord_tricks_test.rb +54 -0
- data/test/ar_test_helper.rb +34 -0
- data/test/caching_test.rb +197 -0
- data/test/dsl_test.rb +162 -0
- data/test/inline_dsl_test.rb +108 -0
- data/test/kashmir_test.rb +216 -0
- data/test/support/ar_models.rb +69 -0
- data/test/support/factories.rb +33 -0
- data/test/support/schema.rb +32 -0
- data/test/test_helper.rb +9 -0
- metadata +217 -0
@@ -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,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
|