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