mongoid-cached-json 1.0

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE.md ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2011 Art.sy, Aaron Windsor, Daniel Doubrovkine
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,138 @@
1
+ Mongoid::CachedJson [![Build Status](https://secure.travis-ci.org/dblock/mongoid-cached-json.png)](http://travis-ci.org/dblock/mongoid-cached-json)
2
+ ===================
3
+
4
+ Typical *as_json* definitions may involve lots of database point queries and method calls. When returning collections of objects, a single call may yield hundreds of database queries that can take seconds. This library mitigates the problem by implementing a module called *CachedJson*.
5
+
6
+ CachedJson enables returning multiple JSON formats from a single class and provides some rules for returning embedded or referenced data. It then uses a scheme where fragments of JSON are cached for a particular (class, id) pair containing only the data that doesn't involve references/embedded documents. To get the full JSON for an instance, CachedJson will combine fragments of JSON from the instance with fragments representing the JSON for its references. In the best case, when all of these fragments are cached, this falls through to a few cache lookups followed by a couple Ruby hash merges to create the JSON.
7
+
8
+ Using Mongoid::CachedJson we were able to cut our JSON API average response time by about a factor of 10.
9
+
10
+ Resources
11
+ ---------
12
+
13
+ * [Need Help?](http://groups.google.com/group/mongoid-cached-json)
14
+ * [Source Code](http://github.com/dblock/mongoid-cached-json)
15
+ * [Travis CI](https://secure.travis-ci.org/dblock/mongoid-cached-json)
16
+
17
+ Quickstart
18
+ ----------
19
+
20
+ Add `mongoid-cached-json` to your Gemfile.
21
+
22
+ gem "mongoid-cached-json"
23
+
24
+ Include `Mongoid::CachedJson` in your models.
25
+
26
+ ``` ruby
27
+ class Gadget
28
+ include Mongoid::CachedJson
29
+
30
+ field :name
31
+ field :extras
32
+
33
+ belongs_to :widget
34
+
35
+ json_fields \
36
+ :name => { },
37
+ :extras => { :properties => :public }
38
+
39
+ end
40
+
41
+ class Widget
42
+ include Mongoid::CachedJson
43
+
44
+ field :name
45
+ has_many :gadgets
46
+
47
+ json_fields \
48
+ :name => { },
49
+ :gadgets => { :type => :reference, :properties => :public }
50
+
51
+ end
52
+ ```
53
+
54
+ Invoke `as_json`.
55
+
56
+ ``` ruby
57
+ # the `:short` version of the JSON, `gadgets` not included
58
+ Widget.first.as_json
59
+
60
+ # equivalent to the above
61
+ Widget.first.as_json({ :properties => :short })
62
+
63
+ # `:public` version of the JSON, `gadgets` returned with `:short` JSON, no `:extras`
64
+ Widget.first.as_json({ :properties => :public })
65
+
66
+ # `:all` version of the JSON, `gadgets` returned with `:all` JSON, including `:extras`
67
+ Widget.first.as_json({ :properties => :all })
68
+ ```
69
+
70
+ Configuration
71
+ -------------
72
+
73
+ By default `Mongoid::CachedJson` will use an instance of `ActiveSupport::Cache::MemoryStore` in a non-Rails and `Rails.cache` in a Rails environment. You can configure it to use any other cache store.
74
+
75
+ ``` ruby
76
+ Mongoid::CachedJson.configure do |config|
77
+ config.cache = ActiveSupport::Cache::FileStore.new
78
+ end
79
+ ```
80
+
81
+ Definining Fields
82
+ -----------------
83
+
84
+ Mongoid::CachedJson supports the following options.
85
+
86
+ * `:hide_as_child_json_when` is an optional function that hides the child JSON from `as_json` parent objects, eg. `cached_json :hide_as_child_json_when => lambda { |instance| ! instance.is_secret? }`
87
+
88
+ Mongoid::CachedJson field definitions support the following options.
89
+
90
+ * `:definition` can be a symbol or an anonymous function, eg. `:description => { :definition => :name }` or `:description => { :definition => lambda { |instance| instance.name } }`
91
+ * `:type` can be `:reference`, required for referenced objects
92
+ * `:properties` can be one of `:short`, `:public`, `:all`, in this order
93
+
94
+ Transformations
95
+ ---------------
96
+
97
+ You can define global transformations on all JSON values with `Mongoid::CachedJson.config.transform`. Each transformation must return a value. In the following example we extend the JSON definition with an application-specific `:trusted` field and encode any content that is not trusted.
98
+
99
+ ``` ruby
100
+ class Widget
101
+ include Mongoid::CachedJson
102
+
103
+ field :name
104
+ field :description
105
+
106
+ json_fields \
107
+ :name => { :trusted => true },
108
+ :description => { }
109
+ end
110
+ ```
111
+
112
+ ``` ruby
113
+ Mongoid::CachedJson.config.transform do |field, definition, value|
114
+ (!! definition[:trusted]) ? value : CGI.escapeHTML(value)
115
+ end
116
+ ```
117
+
118
+ Mixing with Standard as_json
119
+ ----------------------------
120
+
121
+ Taking part in the Mongoid::CachedJson `json_fields` scheme is optional: you can still write *as_json* methods where it makes sense.
122
+
123
+ Turning Caching Off
124
+ -------------------
125
+
126
+ You can set `Mongoid::CachedJson.config.disable_caching = true`. It may be a good idea to set it to `ENV['DISABLE_JSON_CACHING']`, in case this turns out not to be The Solution To All Of Your Performance Problems (TM).
127
+
128
+ Contributing
129
+ ------------
130
+
131
+ Fork the project. Make your feature addition or bug fix with tests. Send a pull request. Bonus points for topical branches.
132
+
133
+ Copyright and License
134
+ ---------------------
135
+
136
+ MIT License, see [LICENSE](LICENSE.md) for details.
137
+
138
+ (c) 2012 [Art.sy Inc.](http://artsy.github.com) and [Contributors](HISTORY.md)
@@ -0,0 +1,7 @@
1
+ # encoding: utf-8
2
+ require 'mongoid'
3
+ require 'active_support/concern'
4
+ require 'mongoid-cached-json/version'
5
+ require 'mongoid-cached-json/config'
6
+ require 'mongoid-cached-json/cached_json'
7
+
@@ -0,0 +1,151 @@
1
+ # encoding: utf-8
2
+ module Mongoid
3
+ module CachedJson
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ class_attribute :all_json_properties
8
+ class_attribute :cached_json_field_defs
9
+ class_attribute :cached_json_reference_defs
10
+ class_attribute :hide_as_child_json_when
11
+ end
12
+
13
+ module ClassMethods
14
+
15
+ # Define JSON fields for a class.
16
+ #
17
+ # @param [ hash ] defs JSON field definition.
18
+ #
19
+ # @since 1.0.0
20
+ def json_fields(defs)
21
+ self.hide_as_child_json_when = defs.delete(:hide_as_child_json_when) || lambda { |a| false }
22
+ self.all_json_properties = [:short, :public, :all]
23
+ cached_json_defs = Hash[defs.map { |k,v| [k, { :type => :callable, :properties => :short, :definition => k }.merge(v)] }]
24
+ self.cached_json_field_defs = {}
25
+ self.cached_json_reference_defs = {}
26
+ self.all_json_properties.each_with_index do |property, i|
27
+ self.cached_json_field_defs[property] = Hash[cached_json_defs.find_all do |field, definition|
28
+ self.all_json_properties.find_index(definition[:properties]) <= i and definition[:type] == :callable
29
+ end]
30
+ self.cached_json_reference_defs[property] = Hash[cached_json_defs.find_all do |field, definition|
31
+ self.all_json_properties.find_index(definition[:properties]) <= i and definition[:type] == :reference
32
+ end]
33
+ # If the field is a reference and is just specified as a symbol, reflect on it to get metadata
34
+ self.cached_json_reference_defs[property].to_a.each do |field, definition|
35
+ if definition[:definition].is_a?(Symbol)
36
+ self.cached_json_reference_defs[property][field][:metadata] = self.reflect_on_association(definition[:definition])
37
+ end
38
+ end
39
+ end
40
+ before_save :expire_cached_json
41
+ end
42
+
43
+ # Given an object definition in the form of either an object or a class, id pair,
44
+ # grab the as_json representation from the cache if possible, otherwise create
45
+ # the as_json representation by loading the object from the database. For any
46
+ # references in the object's JSON representation, we have to recursively materialize
47
+ # the JSON by calling resolve_json_reference on each of them (which may, in turn,
48
+ # call materialize_json)
49
+ def materialize_json(options, object_def)
50
+ return nil if !object_def[:object] and !object_def[:id]
51
+ is_top_level_json = options[:is_top_level_json] || false
52
+ if object_def[:object]
53
+ object_reference = object_def[:object]
54
+ clazz, id = object_def[:object].class, object_def[:object].id
55
+ else
56
+ object_reference = nil
57
+ clazz, id = object_def[:clazz], object_def[:id]
58
+ end
59
+ json = Mongoid::CachedJson.config.cache.fetch(self.cached_json_key(options, clazz, id), { :force => !! Mongoid::CachedJson.config.disable_caching }) do
60
+ object_reference = clazz.where({ :_id => id }).first if !object_reference
61
+ if !object_reference or (!is_top_level_json and options[:properties] != :all and clazz.hide_as_child_json_when.call(object_reference))
62
+ nil
63
+ else
64
+ Hash[clazz.cached_json_field_defs[options[:properties]].map do |field, definition|
65
+ json_value = (definition[:definition].is_a?(Symbol) ? object_reference.send(definition[:definition]) : definition[:definition].call(object_reference))
66
+ Mongoid::CachedJson.config.transform.each do |t|
67
+ json_value = t.call(field, definition, json_value)
68
+ end
69
+ [field, json_value]
70
+ end]
71
+ end
72
+ end
73
+ reference_defs = clazz.cached_json_reference_defs[options[:properties]]
74
+ if !reference_defs.empty?
75
+ object_reference = clazz.where({ :_id => id }).first if !object_reference
76
+ if object_reference and (is_top_level_json or options[:properties] == :all or !clazz.hide_as_child_json_when.call(object_reference))
77
+ json = json.merge(Hash[reference_defs.map do |field, definition|
78
+ json_properties_type = (options[:properties] == :all) ? :all : :short
79
+ [field, clazz.resolve_json_reference(options.merge({ :properties => json_properties_type, :is_top_level_json => false}), object_reference, field, definition)]
80
+ end])
81
+ end
82
+ end
83
+ json
84
+ end
85
+
86
+ # Cache key.
87
+ def cached_json_key(options, cached_class, cached_id)
88
+ "as_json/#{cached_class}/#{cached_id}/#{options[:properties]}/#{!!options[:is_top_level_json]}"
89
+ end
90
+
91
+ # If the reference is a symbol, we may be lucky and be able to figure out the as_json
92
+ # representation by the (class, id) pair definition of the reference. That is, we may
93
+ # be able to load the as_json representation from the cache without even getting the
94
+ # model from the database and materializing it through Mongoid. We'll try to do this first.
95
+ def resolve_json_reference(options, object, field, reference_def)
96
+ reference_json = nil
97
+ if reference_def[:metadata]
98
+ clazz = reference_def[:metadata].class_name.constantize
99
+ key = reference_def[:metadata].key.to_sym
100
+ if reference_def[:metadata].relation == Mongoid::Relations::Referenced::ManyToMany
101
+ reference_json = object.send(key).map do |id|
102
+ materialize_json(options, { :clazz => clazz, :id => id })
103
+ end.compact
104
+ elsif reference_def[:metadata].relation == Mongoid::Relations::Referenced::In
105
+ reference_json = materialize_json(options, { :clazz => clazz, :id => object.send(key) })
106
+ end
107
+ end
108
+ # If we get to this point and reference_json is still nil, there's no chance we can
109
+ # load the JSON from cache so we go ahead and call as_json on the object.
110
+ if !reference_json
111
+ reference = reference_def[:definition].is_a?(Symbol) ? object.send(reference_def[:definition]) : reference_def[:definition].call(object)
112
+ reference_json = reference.as_json(options) if reference
113
+ end
114
+ reference_json
115
+ end
116
+
117
+ end
118
+
119
+ def as_json(options = { :properties => :short })
120
+ raise ArgumentError.new("Missing options[:properties]") if (options.nil? || options[:properties].nil?)
121
+ raise ArgumentError.new("Unknown properties option: #{options[:properties]}") if !self.all_json_properties.member?(options[:properties])
122
+ self.class.materialize_json({ :is_top_level_json => true }.merge(options), { :object => self })
123
+ end
124
+
125
+ # Expire all JSON entries for this class.
126
+ def expire_cached_json
127
+ self.all_json_properties.each do |properties|
128
+ [true, false].each do |is_top_level_json|
129
+ Mongoid::CachedJson.config.cache.delete(self.class.cached_json_key({:properties => properties, :is_top_level_json => is_top_level_json}, self.class, self.id))
130
+ end
131
+ end
132
+ end
133
+
134
+ class << self
135
+
136
+ # Set the configuration options. Best used by passing a block.
137
+ #
138
+ # @example Set up configuration options.
139
+ # Mongoid::CachedJson.configure do |config|
140
+ # config.cache = Rails.cache
141
+ # end
142
+ #
143
+ # @return [ Config ] The configuration obejct.
144
+ def configure
145
+ block_given? ? yield(Mongoid::CachedJson::Config) : Mongoid::CachedJson::Config
146
+ end
147
+ alias :config :configure
148
+ end
149
+
150
+ end
151
+ end
@@ -0,0 +1,94 @@
1
+ # encoding: utf-8
2
+ module Mongoid
3
+ module CachedJson #:nodoc
4
+ module Config
5
+ extend self
6
+ include ActiveSupport::Callbacks
7
+
8
+ attr_accessor :settings, :defaults
9
+ @settings = {}
10
+ @defaults = {}
11
+
12
+ # Define a configuration option with a default.
13
+ #
14
+ # @example Define the option.
15
+ # Config.option(:cache, :default => nil)
16
+ #
17
+ # @param [ Symbol ] name The name of the configuration option.
18
+ # @param [ Hash ] options Extras for the option.
19
+ #
20
+ # @option options [ Object ] :default The default value.
21
+ def option(name, options = {})
22
+ defaults[name] = settings[name] = options[:default]
23
+
24
+ class_eval <<-RUBY
25
+ def #{name}
26
+ settings[#{name.inspect}]
27
+ end
28
+
29
+ def #{name}=(value)
30
+ settings[#{name.inspect}] = value
31
+ end
32
+
33
+ def #{name}?
34
+ #{name}
35
+ end
36
+ RUBY
37
+ end
38
+
39
+ option :disable_caching, { :default => false }
40
+
41
+ # Returns the default cache store, which is either a Rails logger of stdout logger
42
+ #
43
+ # @example Get the default cache store
44
+ # config.default_cache
45
+ #
46
+ # @return [ Cache ] The default Cache instance.
47
+ def default_cache
48
+ defined?(Rails) && Rails.respond_to?(:cache) ? Rails.cache : ::ActiveSupport::Cache::MemoryStore.new
49
+ end
50
+
51
+ # Returns the cache, or defaults to Rails cache or ActiveSupport::Cache::MemoryStore logger.
52
+ #
53
+ # @example Get the cache.
54
+ # config.cache
55
+ #
56
+ # @return [ Cache ] The configured cache or a default cache instance.
57
+ def cache
58
+ settings[:cache] = default_cache unless settings.has_key?(:cache)
59
+ settings[:cache]
60
+ end
61
+
62
+ # Sets the cache to use.
63
+ #
64
+ # @example Set the cache.
65
+ # config.cache = Rails.cache
66
+ #
67
+ # @return [ Cache ] The newly set cache.
68
+ def cache=(cache)
69
+ settings[:cache] = cache
70
+ end
71
+
72
+ # Reset the configuration options to the defaults.
73
+ #
74
+ # @example Reset the configuration options.
75
+ # config.reset!
76
+ def reset!
77
+ settings.replace(defaults)
78
+ end
79
+
80
+ # Define a transformation on JSON data.
81
+ #
82
+ # @example Convert every string in materialized JSON to upper-case.
83
+ # config.transform do |field, value|
84
+ # value.upcase
85
+ # end
86
+ def transform(& block)
87
+ settings[:transform] = [] unless settings.has_key?(:transform)
88
+ settings[:transform] << block if block_given?
89
+ settings[:transform]
90
+ end
91
+
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,7 @@
1
+ # encoding: utf-8
2
+ module Mongoid
3
+ module CachedJson
4
+ VERSION = '1.0'
5
+ end
6
+ end
7
+
metadata ADDED
@@ -0,0 +1,146 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mongoid-cached-json
3
+ version: !ruby/object:Gem::Version
4
+ version: '1.0'
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Aaron Windsor
9
+ - Daniel Doubrovkine
10
+ - Frank Macreery
11
+ autorequire:
12
+ bindir: bin
13
+ cert_chain: []
14
+ date: 2012-02-20 00:00:00.000000000Z
15
+ dependencies:
16
+ - !ruby/object:Gem::Dependency
17
+ name: activesupport
18
+ requirement: &70215312132660 !ruby/object:Gem::Requirement
19
+ none: false
20
+ requirements:
21
+ - - ! '>='
22
+ - !ruby/object:Gem::Version
23
+ version: '0'
24
+ type: :runtime
25
+ prerelease: false
26
+ version_requirements: *70215312132660
27
+ - !ruby/object:Gem::Dependency
28
+ name: mongoid
29
+ requirement: &70215312131960 !ruby/object:Gem::Requirement
30
+ none: false
31
+ requirements:
32
+ - - ! '>='
33
+ - !ruby/object:Gem::Version
34
+ version: '0'
35
+ type: :runtime
36
+ prerelease: false
37
+ version_requirements: *70215312131960
38
+ - !ruby/object:Gem::Dependency
39
+ name: bson_ext
40
+ requirement: &70215312120160 !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ type: :runtime
47
+ prerelease: false
48
+ version_requirements: *70215312120160
49
+ - !ruby/object:Gem::Dependency
50
+ name: hpricot
51
+ requirement: &70215312118740 !ruby/object:Gem::Requirement
52
+ none: false
53
+ requirements:
54
+ - - ! '>='
55
+ - !ruby/object:Gem::Version
56
+ version: '0'
57
+ type: :runtime
58
+ prerelease: false
59
+ version_requirements: *70215312118740
60
+ - !ruby/object:Gem::Dependency
61
+ name: rspec
62
+ requirement: &70215312117220 !ruby/object:Gem::Requirement
63
+ none: false
64
+ requirements:
65
+ - - ~>
66
+ - !ruby/object:Gem::Version
67
+ version: '2.5'
68
+ type: :development
69
+ prerelease: false
70
+ version_requirements: *70215312117220
71
+ - !ruby/object:Gem::Dependency
72
+ name: bundler
73
+ requirement: &70215312116260 !ruby/object:Gem::Requirement
74
+ none: false
75
+ requirements:
76
+ - - ~>
77
+ - !ruby/object:Gem::Version
78
+ version: '1.0'
79
+ type: :development
80
+ prerelease: false
81
+ version_requirements: *70215312116260
82
+ - !ruby/object:Gem::Dependency
83
+ name: jeweler
84
+ requirement: &70215312115520 !ruby/object:Gem::Requirement
85
+ none: false
86
+ requirements:
87
+ - - ~>
88
+ - !ruby/object:Gem::Version
89
+ version: '1.6'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: *70215312115520
93
+ - !ruby/object:Gem::Dependency
94
+ name: yard
95
+ requirement: &70215312114460 !ruby/object:Gem::Requirement
96
+ none: false
97
+ requirements:
98
+ - - ~>
99
+ - !ruby/object:Gem::Version
100
+ version: '0.6'
101
+ type: :development
102
+ prerelease: false
103
+ version_requirements: *70215312114460
104
+ description: Cached-json is a DSL for describing JSON representations of Mongoid models.
105
+ email: dblock@dblock.org
106
+ executables: []
107
+ extensions: []
108
+ extra_rdoc_files:
109
+ - LICENSE.md
110
+ - README.md
111
+ files:
112
+ - lib/mongoid-cached-json.rb
113
+ - lib/mongoid-cached-json/cached_json.rb
114
+ - lib/mongoid-cached-json/config.rb
115
+ - lib/mongoid-cached-json/version.rb
116
+ - LICENSE.md
117
+ - README.md
118
+ homepage: http://github.com/dblock/mongoid-cached-json
119
+ licenses:
120
+ - MIT
121
+ post_install_message:
122
+ rdoc_options: []
123
+ require_paths:
124
+ - lib
125
+ required_ruby_version: !ruby/object:Gem::Requirement
126
+ none: false
127
+ requirements:
128
+ - - ! '>='
129
+ - !ruby/object:Gem::Version
130
+ version: '0'
131
+ segments:
132
+ - 0
133
+ hash: 2087512391400708959
134
+ required_rubygems_version: !ruby/object:Gem::Requirement
135
+ none: false
136
+ requirements:
137
+ - - ! '>='
138
+ - !ruby/object:Gem::Version
139
+ version: '0'
140
+ requirements: []
141
+ rubyforge_project:
142
+ rubygems_version: 1.8.10
143
+ signing_key:
144
+ specification_version: 3
145
+ summary: Effective model-level JSON caching for the Mongoid ODM.
146
+ test_files: []