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
data/Rakefile
ADDED
data/kashmir.gemspec
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'kashmir/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "kashmir"
|
8
|
+
spec.version = Kashmir::VERSION
|
9
|
+
spec.authors = ["IFTTT", "Netto Farah"]
|
10
|
+
spec.email = ["ops@ifttt.com", "nettofarah@gmail.com"]
|
11
|
+
spec.summary = %q{Kashmir is a DSL for quickly defining contracts to decorate your models.}
|
12
|
+
spec.description = %q{
|
13
|
+
Kashmir helps you easily define decorators/representers/presenters for ruby objects.
|
14
|
+
Optionally, Kashmir will also cache these views for faster lookups.
|
15
|
+
}
|
16
|
+
spec.homepage = "http://ifttt.github.io/"
|
17
|
+
spec.license = "MIT"
|
18
|
+
|
19
|
+
spec.files = `git ls-files -z`.split("\x0")
|
20
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
21
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
22
|
+
spec.require_paths = ["lib"]
|
23
|
+
|
24
|
+
spec.add_runtime_dependency "colorize", "~> 0.7"
|
25
|
+
|
26
|
+
spec.add_development_dependency "bundler", "~> 1.7"
|
27
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
28
|
+
spec.add_development_dependency "minitest", "~> 5.0"
|
29
|
+
spec.add_development_dependency "minitest-around", "~> 0.3"
|
30
|
+
spec.add_development_dependency "mocha", "~> 1.1"
|
31
|
+
spec.add_development_dependency "sqlite3", "1.3.10"
|
32
|
+
|
33
|
+
spec.add_development_dependency "dalli", "~> 2.7"
|
34
|
+
spec.add_development_dependency "activerecord", "3.2.8"
|
35
|
+
end
|
data/lib/kashmir.rb
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
require "kashmir/version"
|
2
|
+
require "kashmir/representation"
|
3
|
+
require "kashmir/dsl"
|
4
|
+
require "kashmir/inline_dsl"
|
5
|
+
require "kashmir/plugins/ar"
|
6
|
+
require "kashmir/caching"
|
7
|
+
require "kashmir/representable"
|
8
|
+
|
9
|
+
module Kashmir
|
10
|
+
|
11
|
+
class << self
|
12
|
+
|
13
|
+
attr_accessor :logger
|
14
|
+
|
15
|
+
def included(klass)
|
16
|
+
klass.extend Representable::ClassMethods
|
17
|
+
klass.send(:include, Representable)
|
18
|
+
|
19
|
+
if klass.ancestors.include?(::ActiveRecord::Base)
|
20
|
+
klass.send(:include, Kashmir::AR)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def init(options={})
|
25
|
+
if client = options[:cache_client]
|
26
|
+
@caching = client
|
27
|
+
end
|
28
|
+
|
29
|
+
if logger = options[:logger]
|
30
|
+
@logger = logger
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def logger
|
35
|
+
@logger ||= defined?(Rails) ? Rails.logger : Logger.new(STDOUT)
|
36
|
+
end
|
37
|
+
|
38
|
+
def caching
|
39
|
+
@caching ||= Kashmir::Caching::Null.new
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'kashmir/plugins/memory_caching'
|
2
|
+
require 'kashmir/plugins/null_caching'
|
3
|
+
require 'kashmir/plugins/memcached_caching'
|
4
|
+
require 'colorize'
|
5
|
+
|
6
|
+
module Kashmir
|
7
|
+
module Caching
|
8
|
+
|
9
|
+
def from_cache(representation_definition, object)
|
10
|
+
log("#{"read".blue}: #{log_key(object, representation_definition)}", :debug)
|
11
|
+
|
12
|
+
cached_representation = Kashmir.caching.from_cache(representation_definition, object)
|
13
|
+
|
14
|
+
if cached_representation
|
15
|
+
log("#{"hit".green}: #{log_key(object, representation_definition)}")
|
16
|
+
else
|
17
|
+
log("#{"miss".red}: #{log_key(object, representation_definition)}")
|
18
|
+
end
|
19
|
+
|
20
|
+
cached_representation
|
21
|
+
end
|
22
|
+
|
23
|
+
def bulk_from_cache(representation_definition, objects)
|
24
|
+
class_name = objects.size > 0 ? objects.first.class.to_s : ''
|
25
|
+
log("#{"read_multi".blue}: [#{objects.size}]#{class_name} : #{representation_definition}", :debug)
|
26
|
+
Kashmir.caching.bulk_from_cache(representation_definition, objects)
|
27
|
+
end
|
28
|
+
|
29
|
+
def store_presenter(representation_definition, representation, object, ttl)
|
30
|
+
log("#{"write".blue} TTL: #{ttl}: #{log_key(object, representation_definition)}", :debug)
|
31
|
+
Kashmir.caching.store_presenter(representation_definition, representation, object, ttl)
|
32
|
+
end
|
33
|
+
|
34
|
+
def bulk_write(representation_definition, representations, objects, ttl)
|
35
|
+
class_name = objects.size > 0 ? objects.first.class.to_s : ''
|
36
|
+
log("#{"write_multi".blue}: TTL: #{ttl}: [#{objects.size}]#{class_name} : #{representation_definition}", :debug)
|
37
|
+
Kashmir.caching.bulk_write(representation_definition, representations, objects, ttl)
|
38
|
+
end
|
39
|
+
|
40
|
+
def log_key(object, representation_definition)
|
41
|
+
"#{object.class.name}-#{object.id}-#{representation_definition}"
|
42
|
+
end
|
43
|
+
|
44
|
+
def log(message, level=:info)
|
45
|
+
Kashmir.logger.send(level, ("\n#{"Kashmir::Caching".magenta} #{message}\n"))
|
46
|
+
end
|
47
|
+
|
48
|
+
module_function :from_cache, :bulk_from_cache, :bulk_write, :store_presenter, :log_key, :log
|
49
|
+
end
|
50
|
+
end
|
data/lib/kashmir/dsl.rb
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
module Kashmir
|
2
|
+
module Dsl
|
3
|
+
|
4
|
+
def self.included(klass)
|
5
|
+
klass.extend ClassMethods
|
6
|
+
end
|
7
|
+
|
8
|
+
module ClassMethods
|
9
|
+
|
10
|
+
def prop(name)
|
11
|
+
definitions << name
|
12
|
+
end
|
13
|
+
|
14
|
+
def props(*names)
|
15
|
+
names.each do |name|
|
16
|
+
prop(name)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def group(name, fields)
|
21
|
+
definition = Hash.new
|
22
|
+
definition[name] = fields
|
23
|
+
definitions << definition
|
24
|
+
end
|
25
|
+
|
26
|
+
def embed(name, representer)
|
27
|
+
group(name, representer.definitions)
|
28
|
+
end
|
29
|
+
|
30
|
+
def inline(name, &inline_representer)
|
31
|
+
representer = Kashmir::InlineDsl.build(&inline_representer)
|
32
|
+
embed(name, representer)
|
33
|
+
end
|
34
|
+
|
35
|
+
def definitions
|
36
|
+
@definitions ||= []
|
37
|
+
@definitions
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module SymbolizeHelper
|
2
|
+
def self.symbolize_recursive(hash)
|
3
|
+
{}.tap do |h|
|
4
|
+
hash.each { |key, value| h[key.to_sym] = map_value(value) }
|
5
|
+
end
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.map_value(thing)
|
9
|
+
case thing
|
10
|
+
when Hash
|
11
|
+
symbolize_recursive(thing)
|
12
|
+
when Array
|
13
|
+
thing.map { |v| map_value(v) }
|
14
|
+
else
|
15
|
+
thing
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
# We have to reopen Preloader to allow for it
|
2
|
+
# to accept any random attribute name as a preloadable association.
|
3
|
+
#
|
4
|
+
# This allows us to send any abirtrary Hash to Preloader.
|
5
|
+
# Not only keys that we know are ActiveRecord relations in advance.
|
6
|
+
#
|
7
|
+
|
8
|
+
module ArV4Patch
|
9
|
+
|
10
|
+
def self.included(klass)
|
11
|
+
klass.instance_eval do
|
12
|
+
remove_method :grouped_records
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def grouped_records(association, records)
|
17
|
+
h = {}
|
18
|
+
records.each do |record|
|
19
|
+
next unless record
|
20
|
+
|
21
|
+
unless record.class._reflect_on_association(association)
|
22
|
+
next
|
23
|
+
end
|
24
|
+
|
25
|
+
assoc = record.association(association)
|
26
|
+
klasses = h[assoc.reflection] ||= {}
|
27
|
+
(klasses[assoc.klass] ||= []) << record
|
28
|
+
end
|
29
|
+
h
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
module ArV3Patch
|
34
|
+
|
35
|
+
def self.included(klass)
|
36
|
+
klass.instance_eval do
|
37
|
+
remove_method :records_by_reflection
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def records_by_reflection(association)
|
42
|
+
grouped = records.group_by do |record|
|
43
|
+
reflection = record.class.reflections[association]
|
44
|
+
|
45
|
+
unless reflection
|
46
|
+
next
|
47
|
+
end
|
48
|
+
|
49
|
+
reflection
|
50
|
+
end
|
51
|
+
|
52
|
+
## This takes out the unexisting relations
|
53
|
+
grouped.delete(nil)
|
54
|
+
grouped
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
module ActiveRecord
|
59
|
+
module Associations
|
60
|
+
class Preloader
|
61
|
+
if ActiveRecord::VERSION::STRING >= "4.0.2"
|
62
|
+
include ::ArV4Patch
|
63
|
+
else
|
64
|
+
include ::ArV3Patch
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Kashmir
|
2
|
+
class ActiveRecordRepresentation < Representation
|
3
|
+
|
4
|
+
def present_value(value, arguments, level=1, skip_cache=false)
|
5
|
+
if value.is_a?(Kashmir) || value.is_a?(Kashmir::ArRelation)
|
6
|
+
return value.represent(arguments, level, skip_cache)
|
7
|
+
end
|
8
|
+
|
9
|
+
if value.respond_to?(:represent)
|
10
|
+
value.represent(arguments, level, skip_cache)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require "active_record"
|
2
|
+
require "kashmir/plugins/active_record_representation"
|
3
|
+
require "kashmir/plugins/ar_relation"
|
4
|
+
require "kashmir/patches/active_record"
|
5
|
+
|
6
|
+
module ActiveRecord
|
7
|
+
# = Active Record Relation
|
8
|
+
class Relation
|
9
|
+
include Kashmir::ArRelation
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
# TODO: extract kashmir array module
|
14
|
+
class Array
|
15
|
+
include Kashmir::ArRelation
|
16
|
+
end
|
17
|
+
|
18
|
+
module Kashmir
|
19
|
+
module AR
|
20
|
+
|
21
|
+
def self.included(klass)
|
22
|
+
klass.extend ClassMethods
|
23
|
+
end
|
24
|
+
|
25
|
+
module ClassMethods
|
26
|
+
|
27
|
+
def rep(field, options={})
|
28
|
+
if reflection_names.include?(field)
|
29
|
+
return activerecord_rep(field, options)
|
30
|
+
end
|
31
|
+
|
32
|
+
super
|
33
|
+
end
|
34
|
+
|
35
|
+
def reflection_names
|
36
|
+
if self.respond_to?(:reflections)
|
37
|
+
return reflections.keys.map(&:to_sym)
|
38
|
+
end
|
39
|
+
|
40
|
+
[]
|
41
|
+
end
|
42
|
+
|
43
|
+
def activerecord_rep(field, options)
|
44
|
+
representation = ActiveRecordRepresentation.new(field, options)
|
45
|
+
definitions[field] = representation
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Kashmir
|
2
|
+
module ArRelation
|
3
|
+
|
4
|
+
def represent(representation_definition=[], level=1, skip_cache=false)
|
5
|
+
cached_presenters = Kashmir::Caching.bulk_from_cache(representation_definition, self)
|
6
|
+
|
7
|
+
to_load = []
|
8
|
+
self.zip(cached_presenters).each do |record, cached_presenter|
|
9
|
+
if cached_presenter.nil?
|
10
|
+
to_load << record
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
if to_load.any?
|
15
|
+
ActiveRecord::Associations::Preloader.new(to_load, representation_definition).run
|
16
|
+
end
|
17
|
+
|
18
|
+
to_load_representations = to_load.map do |subject|
|
19
|
+
subject.represent(representation_definition, level, skip_cache) if subject.respond_to?(:represent)
|
20
|
+
end
|
21
|
+
|
22
|
+
if rep = to_load.first and rep.is_a?(Kashmir) and rep.cacheable?
|
23
|
+
Kashmir::Caching.bulk_write(representation_definition, to_load_representations, to_load, level * 60)
|
24
|
+
end
|
25
|
+
|
26
|
+
cached_presenters.compact + to_load_representations
|
27
|
+
end
|
28
|
+
|
29
|
+
def represent_with(&block)
|
30
|
+
map do |subject|
|
31
|
+
subject.represent_with(&block)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
require 'dalli'
|
2
|
+
|
3
|
+
module Kashmir
|
4
|
+
module Caching
|
5
|
+
class Memcached
|
6
|
+
|
7
|
+
attr_reader :client
|
8
|
+
|
9
|
+
def initialize(client, default_ttl = 3600)
|
10
|
+
@client = client
|
11
|
+
@default_ttl = default_ttl
|
12
|
+
end
|
13
|
+
|
14
|
+
def from_cache(definitions, instance)
|
15
|
+
key = presenter_key(definitions, instance)
|
16
|
+
if cached_data = get(key)
|
17
|
+
return cached_data
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def bulk_from_cache(definitions, instances)
|
22
|
+
keys = instances.map do |instance|
|
23
|
+
presenter_key(definitions, instance) if instance.respond_to?(:id)
|
24
|
+
end.compact
|
25
|
+
|
26
|
+
|
27
|
+
# TODO improve this
|
28
|
+
# Creates a hash with all the keys (sorted by the array sort order)
|
29
|
+
# and points everything to null
|
30
|
+
# ex: [a, b, c] -> { a: nil, b: nil, c: nil }
|
31
|
+
results = Hash[keys.map {|x| [x, nil]}]
|
32
|
+
|
33
|
+
# Get results from memcached
|
34
|
+
# This will ONLY return cache hits as a Hash
|
35
|
+
# ex: { a: cached_a, b: cached_b } note that C is not here
|
36
|
+
from_cache = client.get_multi(keys)
|
37
|
+
|
38
|
+
# This assigns each one of the cached values to its keys
|
39
|
+
# preserving cache misses (that still point to nil)
|
40
|
+
from_cache.each_pair do |key, value|
|
41
|
+
results[key] = JSON.parse(value, symbolize_names: true)
|
42
|
+
end
|
43
|
+
|
44
|
+
# returns the cached results in the same order as requested.
|
45
|
+
# this will also return nil values for cache misses
|
46
|
+
results.values
|
47
|
+
end
|
48
|
+
|
49
|
+
def bulk_write(definitions, representations, instances, ttl)
|
50
|
+
client.multi do
|
51
|
+
instances.each_with_index do |instance, index|
|
52
|
+
key = presenter_key(definitions, instance)
|
53
|
+
set(key, representations[index], ttl)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def store_presenter(definitions, representation, instance, ttl=0)
|
59
|
+
key = presenter_key(definitions, instance)
|
60
|
+
set(key, representation, ttl)
|
61
|
+
end
|
62
|
+
|
63
|
+
def presenter_key(definition_name, instance)
|
64
|
+
"#{instance.class}:#{instance.id}:#{definition_name}"
|
65
|
+
end
|
66
|
+
|
67
|
+
def get(key)
|
68
|
+
if data = client.get(key)
|
69
|
+
JSON.parse(data, symbolize_names: true)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def set(key, value, ttl=nil)
|
74
|
+
client.set(key, value.to_json, ttl || @default_ttl)
|
75
|
+
end
|
76
|
+
|
77
|
+
def clear(definition, instance)
|
78
|
+
key = presenter_key(definition, instance)
|
79
|
+
client.delete(key)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|