kashmir 0.1

Sign up to get free protection for your applications and to get access to all the features.
data/Rakefile ADDED
@@ -0,0 +1,11 @@
1
+ require "bundler/gem_tasks"
2
+ require 'rake/testtask'
3
+
4
+ desc 'Test the kashmir gem.'
5
+ task :default => :test
6
+
7
+ Rake::TestTask.new(:test) do |test|
8
+ test.libs << 'test'
9
+ test.test_files = FileList['test/**/*_test.rb']
10
+ test.verbose = true
11
+ end
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
@@ -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,13 @@
1
+ module Kashmir
2
+ module InlineDsl
3
+
4
+ def self.build(&definitions)
5
+ inline_representer = Class.new do
6
+ include Kashmir::Dsl
7
+ end
8
+
9
+ inline_representer.class_eval(&definitions) if block_given?
10
+ inline_representer
11
+ end
12
+ end
13
+ 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