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