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