• Stars
    star
    266
  • Rank 148,523 (Top 4 %)
  • Language
    Ruby
  • License
    MIT License
  • Created about 9 years ago
  • Updated over 2 years ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Kashmir is a Ruby DSL that makes serializing and caching objects a snap.

Open Source at IFTTT

Kashmir

Kashmir is a Ruby DSL that makes serializing and caching objects a snap.

Kashmir allows you to describe representations of Ruby objects. It will generate hashes from these Ruby objects using the representation and dependency tree that you specify.

Kashmir::ActiveRecord will also optimize and try to balance ActiveRecord queries so your application hits the database as little as possible.

Kashmir::Caching builds a dependency tree for complex object representations and caches each level of this tree separately. Kashmir will do so by creating cache views of each level as well as caching a complete tree. The caching engine is smart enough to fill holes in the cache tree with fresh data from your data store.

Combine Kashmir::Caching + Kashmir::ActiveRecord for extra awesomeness.

Example:

For example, a Person with name and age attributes:

  class Person
    include Kashmir
    
    def initialize(name, age)
      @name = name
      @age = age
    end
    
    representations do
      rep :name
      rep :age
    end
  end

could be represented as:

{ name: 'Netto Farah', age: 26 }

Representing an object is as simple as:

  1. Add include Kashmir to the target class.
  2. Whitelist all the fields you want to include in a representation.
# Add fields and methods you want to be visible to Kashmir
representations do
  rep(:name)
  rep(:age)
end
  1. Instantiate an object and #represent it.
# Pass in an array with all the fields you want included
Person.new('Netto Farah', 26).represent([:name, :age]) 
 => {:name=>"Netto Farah", :age=>"26"} 

Installation

Add this line to your application's Gemfile:

gem 'kashmir'

And then execute:

$ bundle

Usage

Kashmir is better described with examples.

Basic Representations

Describing an Object

Only whitelisted fields can be represented by Kashmir. This is done so sensitive fields (like passwords) cannot be accidentally exposed to clients.

class Recipe < OpenStruct
  include Kashmir

  representations do
    rep(:title)
    rep(:preparation_time)
  end
end

Instantiate a Recipe:

recipe = Recipe.new(title: 'Beef Stew', preparation_time: 60)

Kashmir automatically adds a #represent method to every instance of Recipe. #represent takes an Array with all the fields you want as part of your representation.

recipe.represent([:title, :preparation_time])
=> { title: 'Beef Stew', preparation_time: 60 }

Calculated Fields

You can represent any instance variable or method (basically anything that returns true for respond_to?).

class Recipe < OpenStruct
  include Kashmir

  representations do
    rep(:title)
    rep(:num_steps)
  end
  
  def num_steps
    steps.size
  end
end
Recipe.new(title: 'Beef Stew', steps: ['chop', 'cook']).represent([:title, :num_steps])
=> { title: 'Beef Stew', num_steps: 2 }

Nested Representations

You can nest Kashmir objects to represent complex relationships between your objects.

class Recipe < OpenStruct
  include Kashmir

  representations do
    rep(:title)
    rep(:chef)
  end
end

class Chef < OpenStruct
  include Kashmir

  representations do
    base([:name])
  end
end

When you create a representation, nest hashes to create nested representations.

netto = Chef.new(name: 'Netto Farah')
beef_stew = Recipe.new(title: 'Beef Stew', chef: netto)

beef_stew.represent([:title, { :chef => [ :name ] }])
=> {
  :title => "Beef Stew",
  :chef => {
    :name => 'Netto Farah'
  }
}

Not happy with this syntax? Check out Kashmir::DSL or Kashmir::InlineDSL for prettier code.

Base Representations

Are you tired of repeating the same fields over and over? You can create a base representation of your objects, so Kashmir returns basic fields automatically.

class Recipe
  include Kashmir
  
  representations do
    base [:title, :preparation_time]
    rep :num_steps
    rep :chef
  end
end

base(...) takes an array with the fields you want to return on every representation of a given class.

brisket = Recipe.new(title: 'BBQ Brisket', preparation_time: 'a long time')
brisket.represent()
=> { :title => 'BBQ Brisket', :preparation_time => 'a long time' }

Complex Representations

You can nest as many Kashmir objects as you want.

class Recipe < OpenStruct
  include Kashmir

  representations do
    base [:title]
    rep :chef
  end
end

class Chef < OpenStruct
  include Kashmir

  representations do
    base :name
    rep :restaurant
  end
end

class Restaurant < OpenStruct
  include Kashmir

  representations do
    base [:name]
    rep :rating
  end
end
bbq_joint = Restaurant.new(name: "Netto's BBQ Joint", rating: '5 Stars')
netto = Chef.new(name: 'Netto', restaurant: bbq_joint)
brisket = Recipe.new(title: 'BBQ Brisket', chef: netto)

brisket.represent([
  :chef => [
    { :restaurant => [ :rating ] }
  ]
])

=> {
  title: 'BBQ Brisket',
  chef: {
    name: 'Netto',
    restaurant: {
      name: "Netto's BBQ Joint",
      rating: '5 Stars'
    }
  }
}

Collections

Arrays of Kashmir objects work the same way as any other Kashmir representations. Kashmir will augment Array with #represent that will represent every item in the array.

class Ingredient < OpenStruct
  include Kashmir

  representations do
    rep(:name)
    rep(:quantity)
  end
end

class ClassyRecipe < OpenStruct
  include Kashmir

  representations do
    rep(:title)
    rep(:ingredients)
  end
end
omelette = ClassyRecipe.new(title: 'Omelette Du Fromage')
omelette.ingredients = [
  Ingredient.new(name: 'Egg', quantity: 2),
  Ingredient.new(name: 'Cheese', quantity: 'a lot!')
]

Just describe your Array representations like any regular nested representation.

omelette.represent([:title, { 
    :ingredients => [ :name, :quantity ]
  }
])
=> {
  title: 'Omelette Du Fromage',
  ingredients: [
    { name: 'Egg', quantity: 2 },
    { name: 'Cheese', quantity: 'a lot!' }
  ]
}

Kashmir::Dsl

Passing arrays and hashes around can be very tedious and lead to duplication. Kashmir::Dsl allows you to create your own representers/decorators so you can keep your logic in one place and make way more expressive.

class Recipe < OpenStruct
  include Kashmir

  representations do
    rep(:title)
    rep(:num_steps)
  end
end

class RecipeRepresenter
  include Kashmir::Dsl

  prop :title
  prop :num_steps
end

All you need to do is include Kashmir::Dsl in any ruby class. Every call to prop(field_name) will translate directly into just adding an extra field in the representation array.

In this case, RecipeRepresenter will translate directly to [:title, :num_steps].

brisket = Recipe.new(title: 'BBQ Brisket', num_steps: 2)
brisket.represent(RecipePresenter)

=>  { title: 'BBQ Brisket', num_steps: 2 }

Embedded Representers

It is also possible to define nested representers with embed(:property_name, RepresenterClass).

class RecipeWithChefRepresenter
  include Kashmir::Dsl

  prop :title
  embed :chef, ChefRepresenter
end

class ChefRepresenter
  include Kashmir::Dsl
  
  prop :full_name
end

Kashmir will inline these classes and return a raw Kashmir description.

RecipeWithChefRepresenter.definitions == [ :title, { :chef => [ :full_name ] }]
=> true

Representing the objects will work just as before.

chef = Chef.new(first_name: 'Netto', last_name: 'Farah')
brisket = Recipe.new(title: 'BBQ Brisket', chef: chef)

brisket.represent(RecipeWithChefRepresenter)
 
=> {
  title: 'BBQ Brisket',
  chef: {
    full_name: 'Netto Farah'
  }
}

Inline Representers

You don't necessarily need to define a class for every nested representation.

class RecipeWithInlineChefRepresenter
  include Kashmir::Dsl

  prop :title

  inline :chef do
    prop :full_name
  end
end

Using inline(:property_name, &block) will work the same way as embed. Except that you can now define short representations using ruby blocks. Leading us to our next topic.

Kashmir::InlineDsl

Kashmir::InlineDsl sits right in between raw representations and Representers. It reads much better than arrays of hashes and provides the expressiveness of Kashmir::Dsl without all the ceremony.

It works with every feature from Kashmir::Dsl and allows you to define quick inline descriptions for your Kashmir objects.

class Recipe < OpenStruct
  include Kashmir

  representations do
    rep(:title)
    rep(:num_steps)
  end
end

Just call #represent_with(&block) on any Kashmir object and use the Kashmir::Dsl syntax.

brisket = Recipe.new(title: 'BBQ Brisket', num_steps: 2)

brisket.represent_with do
  prop :title
  prop :num_steps
end

=> { title: 'BBQ Brisket', num_steps: 2 }

Nested Inline Representations

You can nest inline representations using inline(:field, &block) the same way we did with Kashmir::Dsl.

class Ingredient < OpenStruct
  include Kashmir

  representations do
    rep(:name)
    rep(:quantity)
  end
end

class ClassyRecipe < OpenStruct
  include Kashmir

  representations do
    rep(:title)
    rep(:ingredients)
  end
end
omelette = ClassyRecipe.new(title: 'Omelette Du Fromage')
omelette.ingredients = [
  Ingredient.new(name: 'Egg', quantity: 2),
  Ingredient.new(name: 'Cheese', quantity: 'a lot!')
]

Just call #represent_with(&block) and start nesting other inline representations.

omelette.represent_with do
  prop :title
  inline :ingredients do
    prop :name
    prop :quantity
  end
end

=> {
  title: 'Omelette Du Fromage',
  ingredients: [
    { name: 'Egg', quantity: 2 },
    { name: 'Cheese', quantity: 'a lot!' }
  ]
}

Inline representations can become lengthy and confusing over time. If you find yourself nesting more than two levels or including more than 3 or 4 fields per level consider creating Representers with Kashmir::Dsl.

Kashmir::ActiveRecord

Kashmir works just as well with ActiveRecord. ActiveRecord::Relations can be used as Kashmir representations just as any other classes.

Kashmir will attempt to preload every ActiveRecord::Relation defined as representations automatically by using ActiveRecord::Associations::Preloader. This will guarantee that you don't run into N+1 queries while representing collections and dependent objects.

Here's an example of how Kashmir will attempt to optimize database queries:

ActiveRecord::Schema.define do
  create_table :recipes, force: true do |t|
    t.column :title, :string
    t.column :num_steps, :integer
    t.column :chef_id, :integer
  end
  
  create_table :chefs, force: true do |t|
    t.column :name, :string
  end
end
module AR
  class Recipe < ActiveRecord::Base
    include Kashmir

    belongs_to :chef

    representations do
      rep :title
      rep :chef
    end
  end

  class Chef < ActiveRecord::Base
    include Kashmir

    has_many :recipes

    representations do
      rep :name
      rep :recipes
    end
  end
end
AR::Chef.all.each do |chef|
  chef.recipes.to_a
end

will generate

SELECT * FROM chefs
SELECT "recipes".* FROM "recipes" WHERE "recipes"."chef_id" = ?
SELECT "recipes".* FROM "recipes" WHERE "recipes"."chef_id" = ?

With Kashmir:

AR::Chef.all.represent([:recipes])
SELECT "chefs".* FROM "chefs"
SELECT "recipes".* FROM "recipes" WHERE "recipes"."chef_id" IN (1, 2)

For more examples, check out: https://github.com/IFTTT/kashmir/blob/master/test/activerecord_tricks_test.rb

Kashmir::Caching (Experimental)

Caching is the best feature in Kashmir. The Kashmir::Caching module will cache every level of the dependency tree Kashmir generates when representing an object.

Dependency Tree

As you can see in the image above, Kashmir will build a dependency tree of the representation. If you have Caching on, Kashmir will:

  • Build a cache key for each individual object (green)
  • Wrap complex dependencies into their on cache key (blue and pink)
  • Wrap the whole representation into one unique cache key (red)

Each layer gets its own cache keys which can be expired at different times. Kashmir will also be able to fill in blanks in the dependency tree and fetch missing objects individually.

Caching is turned off by default, but you can use one of the two available implementations.

You can also build your own custom caching engine by following the NullCaching protocol available at: https://github.com/IFTTT/kashmir/blob/master/lib/kashmir/plugins/null_caching.rb

Enabling Kashmir::Caching

In Memory
Kashmir.init(
  cache_client: Kashmir::Caching::Memory.new
)
With Memcached
require 'kashmir/plugins/memcached_caching'

client = Dalli::Client.new(url, namespace: 'kashmir', compress: true)
default_ttl = 5.minutes

Kashmir.init(
  cache_client: Kashmir::Caching::Memcached.new(client, default_ttl)
)

For more advanced examples, check out: https://github.com/IFTTT/kashmir/blob/master/test/caching_test.rb

Contributing

  1. Fork it ( https://github.com/[my-github-username]/kashmir/fork )
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create a new Pull Request

More Repositories

1

JazzHands

A simple keyframe-based animation framework for UIKit. Perfect for scrolling app intros.
Objective-C
6,419
star
2

RazzleDazzle

A simple keyframe-based animation framework for iOS, written in Swift. Perfect for scrolling app intros.
Swift
3,361
star
3

FastttCamera

Fasttt and easy camera framework for iOS with customizable filters
Objective-C
1,853
star
4

jot

An iOS framework for easily adding drawings and text to images.
Objective-C
1,775
star
5

polo

Polo travels through your database and creates sample snapshots so you can work with real world data in development.
Ruby
758
star
6

SparkleMotion

A ViewPager animator that animates Views within pages as well as views across pages.
Java
340
star
7

IFTTTLaunchImage

Put your asset catalog launch images to work for you.
Objective-C
310
star
8

tablerize

Say goodbye to aligning tables in Markdown.
Ruby
40
star
9

ConnectSDK-Android

Android SDK for Connect API
Java
23
star
10

connect_with_ifttt_auth_sample

A fake OAuth2 server, mobile API and IFTTT Channel to demonstrate how to implement the server side portion of the Connect with IFTTT SDK.
JavaScript
20
star
11

ConnectSDK-iOS

Connect Button SDK for iOS
Swift
16
star
12

redshift_runner

Ruby gem to query AWS Redshift
Ruby
15
star
13

kramdown-tablerize

Use Tablerize to convert YAML tables in Markdown to HTML.
Ruby
7
star
14

heroku_line_item_billing

Ruby
5
star
15

stripe-webhook-event-ingester

An AWS CDK-based project for ingesting Stripe webhook events into an SQS queue
Python
4
star
16

service-api

OpenAPI definition for IFTTT Service API
HTML
4
star
17

memoize_via_cache

Ruby
4
star
18

pool-shard

Pools of sharded PostgreSQL database connections.
CoffeeScript
4
star
19

elb-tool

Go
2
star
20

delayed_job_shim_for_resque

Ruby
2
star
21

getting-started-with-connect

Getting Started with IFTTT Connect
Python
2
star
22

github_channel_test

1
star
23

3d-logo

IFTTT logo, suitable for 3D printing
OpenSCAD
1
star
24

engineering-handbook

1
star