darstellung 0.0.0 → 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -1,6 +1,185 @@
1
1
  Darstellung [![Build Status](https://secure.travis-ci.org/durran/darstellung.png?branch=master&.png)](http://travis-ci.org/durran/darstellung) [![Code Climate](https://codeclimate.com/badge.png)](https://codeclimate.com/github/durran/darstellung)
2
2
  ========
3
3
 
4
+ Darstellung is a simple DSL for defining what should be displayed in
5
+ resource representations most of the time in API consumption. The
6
+ library is currently in an experimental phase (pre 1.0.0).
7
+
8
+ Usage
9
+ -----
10
+
11
+ Say we have a `UserResource` that is responsible for returning
12
+ representations of `User` models. With Darstellung, we tell it what
13
+ fields to display in a "detail" representation and in a "summary"
14
+ representation:
15
+
16
+ ```ruby
17
+ class UserResource
18
+ include Darstellung::Representable
19
+
20
+ summary :username
21
+
22
+ detail :first_name
23
+ detail :last_name
24
+ end
25
+ ```
26
+
27
+ Then we can initialize a new `UserResource` with a single `User` and ask for
28
+ the hash representation back:
29
+
30
+ ```ruby
31
+ resource = UserResource.new(user)
32
+ resource.detail #=> { version: "none", resource: { first_name: "john", last_name: "doe" }}
33
+ resource.summary #=> { version: "none", resource: { username: "john" }}
34
+ ```
35
+
36
+ If we provide the `UserResource` with an `Enumerable` of `User`s, we can ask
37
+ for a collection, which returns an array of summary representations for each
38
+ user in the list.
39
+
40
+ ```ruby
41
+ resource = UserResource.new([ user_one, user_two ])
42
+ resource.collection #=> { version: "none", resource: [{ username: "john" }, { username: "joe" }]}
43
+ ```
44
+
45
+ Versioning
46
+ ----------
47
+
48
+ Just like any other piece of software, your application's API is a contract
49
+ for others to use, and changes to this contract should follow a sane and
50
+ predictable pattern. Darstellung handles this by allowing you to specify versions
51
+ in which various attributes are displayed in the detail and summary views.
52
+ Clients can request a specific version of the API and get the expected results
53
+ back at all times. It is expected that the version numbers follow the
54
+ Semantic Versioning Specification in order to maintain some consistency.
55
+
56
+ Here is a `UserResource` with versioning:
57
+
58
+ ```ruby
59
+ class UserResource
60
+ include Darstellung::Representable
61
+
62
+ summary :username
63
+ summary :created_at, from: "1.0.1"
64
+
65
+ detail :first_name
66
+ detail :last_name, from: "1.0.5", to: "2.0.0"
67
+ end
68
+ ```
69
+
70
+ If we pass a version to the `detail`, `summary`, and `collection` methods on
71
+ the resource, we will only get back attributes that fall in line with the
72
+ version specified:
73
+
74
+ ```ruby
75
+ resource = UserResource.new(user)
76
+ resource.detail("1.0.0")
77
+ #=> { version: "1.0.0", resource: { first_name: "john" }}
78
+ resource.detail("1.0.5")
79
+ #=> { version: "1.0.5", resource: { first_name: "john", last_name: "doe" }}
80
+
81
+ resource.summary("1.0.0")
82
+ #=> { version: "1.0.0", resource: { username: "john" }}
83
+ resource.summary("2.0.0")
84
+ #=> { version: "2.0.0", resource: { username: "john", created_at: "2012-1-1" }}
85
+
86
+ resource = UserResource.new([ user_one, user_two ])
87
+ resource.collection("2.0.0")
88
+ # => {
89
+ # version: "2.0.0",
90
+ # resource: [
91
+ # { username: "john", created_at: "2012-1-1" },
92
+ # { username: "joe", created_at: "2012-1-2" }
93
+ # ]
94
+ # }
95
+ ```
96
+
97
+ Reasoning
98
+ ---------
99
+
100
+ This is simply a case of SRP and maintainability. While some may argue against
101
+ SRP in Rails being overkill, I disagree and will simply show examples showing
102
+ the choices and the developer can decide for themselves. Although my personal
103
+ preference would be to use Sinatra or Webmachine in these API cases, there's a good
104
+ [blog post](http://blog.gomiso.com/2011/05/16/if-youre-using-to_json-youre-doing-it-wrong)
105
+ from the authors of RABL discussing how this gets out of hand quickly in Rails.
106
+
107
+ Speed
108
+ -----
109
+
110
+ Bypassing Active Model's serialization is going to provide you with a huge
111
+ performance benefit. Let's look at a basic Mongoid model:
112
+
113
+ ```ruby
114
+ class Band
115
+ include Mongoid::Document
116
+ field :description, type: String
117
+ field :formed_on, type: Date
118
+ field :location, type: String
119
+ field :genres, type: Array, default: []
120
+ field :name, type: String
121
+ field :similarities, type: Array, default: []
122
+ field :sounds, type: Array, default: []
123
+ field :website, type: String
124
+ end
125
+ ```
126
+
127
+ Now let's serialize the model to json (using YAJL), 100,000 times:
128
+
129
+ ```ruby
130
+ bench.report do
131
+ 100_000.times do
132
+ band.to_json
133
+ end
134
+ end
135
+ ```
136
+ ```
137
+ user system total real
138
+ 37.150000 0.100000 37.250000 ( 37.250232)
139
+ ```
140
+
141
+ When we create a resource for the `Band` with Darstellung and serialize it
142
+ that way, we get some serious improvement (over 6x faster).
143
+
144
+ ```ruby
145
+ class BandResource
146
+ include Darstellung::Representable
147
+ detail :description
148
+ detail :formed_on
149
+ detail :location
150
+ detail :genres
151
+ detail :name
152
+ detail :similarities
153
+ detail :sounds
154
+ detail :website
155
+ end
156
+
157
+ bench.report do
158
+ 100_000.times do
159
+ BandResource.new(band).detail.to_json
160
+ end
161
+ end
162
+ ```
163
+
164
+ ```
165
+ user system total real
166
+ 6.270000 0.010000 6.280000 ( 6.277472)
167
+ ```
168
+
169
+ The results are drastically different in the simplest of examples, and expect
170
+ another order of magnitude gain when including relations.
171
+
172
+ Serialization
173
+ -------------
174
+
175
+ Darstellung does not deal in serialization at all. It's only purpose is to
176
+ provide hash representations of your API resources for specific versions. It's
177
+ up to you to call `to_json` or `to_xml` on them, using whatever serialization
178
+ library you want.
179
+
180
+ License
181
+ -------
182
+
4
183
  Copyright (c) 2012 Durran Jordan
5
184
 
6
185
  Permission is hereby granted, free of charge, to any person obtaining
@@ -0,0 +1,3 @@
1
+ # encoding: utf-8
2
+ require "darstellung/representable"
3
+ require "darstellung/version"
@@ -0,0 +1,79 @@
1
+ # encoding: utf-8
2
+ module Darstellung
3
+
4
+ # An attribute is any field that can be represented. This class provides
5
+ # extra behavior around when and how these fields get represented.
6
+ #
7
+ # @since 0.0.0
8
+ class Attribute
9
+
10
+ # @attribute [r] name The name of the attribute.
11
+ # @attribute [r] options The attribute options.
12
+ # @attribute [r] block The block to call to get the value.
13
+ attr_reader :name, :options, :block
14
+
15
+ # Determines if the attribute is displayable in the representation given
16
+ # the provided version.
17
+ #
18
+ # @example Is the attribute displayable?
19
+ # attribute.displayable?("1.0.0")
20
+ #
21
+ # @note This method assumes that API versions are following the Semantic
22
+ # Versioning Specificaion, and does its comparison of version strings
23
+ # with this in mind.
24
+ #
25
+ # @param [ String ] version The version number.
26
+ #
27
+ # @return [ true, false ] If the attribute is displayable.
28
+ #
29
+ # @see http://semver.org/
30
+ #
31
+ # @since 0.0.0
32
+ def displayable?(version)
33
+ return true unless version
34
+ from <= version && version <= to(version)
35
+ end
36
+
37
+ # Initialize the new attribute.
38
+ #
39
+ # @example Initialize the new attribute.
40
+ # Darstellung::Attribute.new(:name, version: "1.0.1")
41
+ #
42
+ # @param [ Symbol ] name The name of the attribute.
43
+ # @param [ Hash ] options The attribute options.
44
+ #
45
+ # @option options [ String ] :from The version the attribute is available
46
+ # from.
47
+ # @option options [ String ] :to The version the attribute is available
48
+ # to.
49
+ #
50
+ # @since 0.0.0
51
+ def initialize(name, options = {}, &block)
52
+ @name, @options, @block = name, options, block
53
+ end
54
+
55
+ # Get the value for the attribute from the provided resource.
56
+ #
57
+ # @example Get the value for the attribute.
58
+ # attribute.value(user)
59
+ #
60
+ # @param [ Object ] resource The resource to execute on.
61
+ #
62
+ # @return [ Object ] The value of the resource.
63
+ #
64
+ # @since 0.0.0
65
+ def value(resource)
66
+ block ? block.call(resource) : resource.__send__(name)
67
+ end
68
+
69
+ private
70
+
71
+ def from
72
+ options[:from] || "0.0.0"
73
+ end
74
+
75
+ def to(version)
76
+ options[:to] || "#{version}+"
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,34 @@
1
+ # encoding: utf-8
2
+ module Darstellung
3
+
4
+ # This module contains access from the instance level to the attribute
5
+ # definitions provided by the macros.
6
+ #
7
+ # @since 0.0.0
8
+ module Definable
9
+
10
+ # Get all the attributes that are used in the detail representation.
11
+ #
12
+ # @example Get all the detail attributes fields.
13
+ # user_resource.detail_attributes
14
+ #
15
+ # @return [ Hash ] The name/attribute pairs.
16
+ #
17
+ # @since 0.0.0
18
+ def detail_attributes
19
+ self.class.detail_attributes
20
+ end
21
+
22
+ # Get all the attributes that are used in the summary representation.
23
+ #
24
+ # @example Get all the summary attributes fields.
25
+ # user_resource.summary_attributes
26
+ #
27
+ # @return [ Hash ] The name/attribute pairs.
28
+ #
29
+ # @since 0.0.0
30
+ def summary_attributes
31
+ self.class.summary_attributes
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,138 @@
1
+ # encoding: utf-8
2
+ require "darstellung/registry"
3
+
4
+ module Darstellung
5
+
6
+ # This module provides all the class level macros for defining
7
+ # representations.
8
+ #
9
+ # @since 0.0.0
10
+ module Macros
11
+
12
+ # Defines an attribute to be displayed in the detail representation of the
13
+ # resource.
14
+ #
15
+ # @example Display the name field in the detail display.
16
+ # class UserResource
17
+ # include Darstellung::Representable
18
+ # detail :name
19
+ # end
20
+ #
21
+ # @example Display the name field only from version 1.0.0
22
+ # class UserResource
23
+ # include Darstellung::Representable
24
+ # detail :name, from: "1.0.0"
25
+ # end
26
+ #
27
+ # @example Display the name field only from version 1.0.0 - 1.1.0
28
+ # class UserResource
29
+ # include Darstellung::Representable
30
+ # detail :name, from: "1.0.0", to: "1.1.0"
31
+ # end
32
+ #
33
+ # @example Display the name field as a custom representation.
34
+ # class UserResource
35
+ # include Darstellung::Representable
36
+ #
37
+ # detail :name do |user|
38
+ # user.full_name
39
+ # end
40
+ # end
41
+ #
42
+ # @param [ Symbol ] name The name of the attribute.
43
+ # @param [ Hash ] options The attribute options.
44
+ #
45
+ # @option options [ String ] :from The version the field is available from.
46
+ # @option options [ String ] :to The version the field is available to.
47
+ #
48
+ # @return [ Attribute ] The attribute object for the field.
49
+ #
50
+ # @since 0.0.0
51
+ def detail(name, options = {}, &block)
52
+ create_attribute(name, detail_attributes, options, &block)
53
+ end
54
+
55
+ # Get all the attributes that are used in the detail representation.
56
+ #
57
+ # @example Get all the detail attributes fields.
58
+ # class UserResource
59
+ # include Darstellung::Representable
60
+ # end
61
+ #
62
+ # UserResource.detail_attributes
63
+ #
64
+ # @return [ Hash ] The name/attribute pairs.
65
+ #
66
+ # @since 0.0.0
67
+ def detail_attributes
68
+ @detail_attributes ||= {}
69
+ end
70
+
71
+ # Defines an attribute to be displayed in the summary representation of the
72
+ # resource.
73
+ #
74
+ # @example Display the name field in the summary display.
75
+ # class UserResource
76
+ # include Darstellung::Representable
77
+ # summary :name
78
+ # end
79
+ #
80
+ # @example Display the name field only from version 1.0.0
81
+ # class UserResource
82
+ # include Darstellung::Representable
83
+ # summary :name, from: "1.0.0"
84
+ # end
85
+ #
86
+ # @example Display the name field only from version 1.0.0 - 1.1.0
87
+ # class UserResource
88
+ # include Darstellung::Representable
89
+ # summary :name, from: "1.0.0", to: "1.1.0"
90
+ # end
91
+ #
92
+ # @example Display the name field as a custom representation.
93
+ # class UserResource
94
+ # include Darstellung::Representable
95
+ #
96
+ # summary :name do |user|
97
+ # user.full_name
98
+ # end
99
+ # end
100
+ #
101
+ # @param [ Symbol ] name The name of the attribute.
102
+ # @param [ Hash ] options The attribute options.
103
+ #
104
+ # @option options [ String ] :from The version the field is available from.
105
+ # @option options [ String ] :to The version the field is available to.
106
+ #
107
+ # @return [ Attribute ] The attribute object for the field.
108
+ #
109
+ # @since 0.0.0
110
+ def summary(name, options = {}, &block)
111
+ create_attribute(name, summary_attributes, options, &block)
112
+ end
113
+
114
+ # Get all the attributes that are used in the summary representation.
115
+ #
116
+ # @example Get all the summary attributes fields.
117
+ # class UserResource
118
+ # include Darstellung::Representable
119
+ # end
120
+ #
121
+ # UserResource.summary_attributes
122
+ #
123
+ # @return [ Hash ] The name/attribute pairs.
124
+ #
125
+ # @since 0.0.0
126
+ def summary_attributes
127
+ @summary_attributes ||= {}
128
+ end
129
+
130
+ private
131
+
132
+ def create_attribute(name, attributes, options = {}, &block)
133
+ normalized = name.to_sym
134
+ attributes[normalized] = Attribute.new(normalized, options, &block)
135
+ Registry.register(options)
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,85 @@
1
+ # encoding: utf-8
2
+ module Darstellung
3
+
4
+ # Contains information on all versions in the API that have been registered.
5
+ # This is determined from the various "from" and "to" options provided to the
6
+ # macros.
7
+ #
8
+ # @since 0.0.0
9
+ module Registry
10
+ extend self
11
+
12
+ # Register a set of options provided to a representation macro.
13
+ #
14
+ # @example Register the options.
15
+ # Dartellung::Registry.register(from: "1.0.0", to: "2.0.0")
16
+ #
17
+ # @note This takes the "from" and "to" options and registers them as
18
+ # official API versions.
19
+ #
20
+ # @param [ Hash ] options The macro options.
21
+ #
22
+ # @return [ Array<String> ] All the registered versions.
23
+ #
24
+ # @since 0.0.0
25
+ def register(options)
26
+ from, to = options[:from], options[:to]
27
+ registered_versions[from] = true if from
28
+ registered_versions[to] = true if to
29
+ versions
30
+ end
31
+
32
+ # Is a particular version registered with the API?
33
+ #
34
+ # @example Check if the version is registered.
35
+ # Darstellung::Registry.registered?("2.1.5")
36
+ #
37
+ # @param [ String ] version The version to check.
38
+ #
39
+ # @return [ true, false ] If the version is registered.
40
+ #
41
+ # @since 0.0.0
42
+ def registered?(version)
43
+ registered_versions[version]
44
+ end
45
+
46
+ # Validate that the provided version is registered.
47
+ #
48
+ # @example Validate the version.
49
+ # Darstellung::Registry.validate!("1.0.5")
50
+ #
51
+ # @param [ String ] version The version to validate.
52
+ #
53
+ # @raise [ NotRegistered ] If the version is not registered.
54
+ #
55
+ # @since 0.0.0
56
+ def validate!(version)
57
+ unless version.nil? || registered?(version)
58
+ raise NotRegistered.new("#{version} is not a valid API version.")
59
+ end
60
+ end
61
+
62
+ # Provides a list of all registered versions in the API.
63
+ #
64
+ # @example List all versions.
65
+ # Darstellung::Registry.versions
66
+ #
67
+ # @return [ Array<String> ] All the registered versions.
68
+ #
69
+ # @since 0.0.0
70
+ def versions
71
+ registered_versions.keys
72
+ end
73
+
74
+ # Raised when validating a version that does not exist in the registry.
75
+ #
76
+ # @since 0.0.0
77
+ class NotRegistered < Exception; end
78
+
79
+ private
80
+
81
+ def registered_versions
82
+ @registered_versions ||= {}
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,126 @@
1
+ # encoding: utf-8
2
+ require "darstellung/attribute"
3
+ require "darstellung/definable"
4
+ require "darstellung/macros"
5
+
6
+ module Darstellung
7
+
8
+ # This module is included into resources that need a summary and detail view
9
+ # for display in APIs. Summary views are for display in lists, detail views
10
+ # are generally show actions.
11
+ #
12
+ # @since 0.0.0
13
+ module Representable
14
+ include Definable
15
+
16
+ # @attribute [r] resource The resource being represented.
17
+ attr_reader :resource
18
+
19
+ # Gets the collection view for a specific version of the resource. If no
20
+ # version is provided then we assume from "0.0.0" which will render
21
+ # attributes available in all versions of the API.
22
+ #
23
+ # @example Get the collection representation.
24
+ # user_resource.collection("1.0.1")
25
+ #
26
+ # @note The collection representation is a list of summary representations.
27
+ #
28
+ # @param [ String ] version The version to get of the resource.
29
+ #
30
+ # @return [ Hash ] The collection representation of the resource.
31
+ #
32
+ # @since 0.0.0
33
+ def collection(version = nil)
34
+ representation(version, multiple(summary_attributes, version))
35
+ end
36
+
37
+ # Gets the detail view for a specific version of the resource. If no
38
+ # version is provided then we assume from "0.0.0" which will render
39
+ # attributes available in all versions of the API.
40
+ #
41
+ # @example Get the detail representation.
42
+ # user_resource.detail("1.0.1")
43
+ #
44
+ # @param [ String ] version The version to get of the resource.
45
+ #
46
+ # @return [ Hash ] The detail representation of the resource.
47
+ #
48
+ # @since 0.0.0
49
+ def detail(version = nil)
50
+ representation(version, single(detail_attributes, resource, version))
51
+ end
52
+
53
+ # Initialize the new representation with the provided resource.
54
+ #
55
+ # @example Initialize the representation.
56
+ # class UserResource
57
+ # include Darstellung::Representable
58
+ # end
59
+ #
60
+ # UserResource.new(user)
61
+ #
62
+ # @param [ Object ] resource The resource to be represented.
63
+ #
64
+ # @since 0.0.0
65
+ def initialize(resource)
66
+ @resource = resource
67
+ end
68
+
69
+ # Gets the summary view for a specific version of the resource. If no
70
+ # version is provided then we assume from "0.0.0" which will render
71
+ # attributes available in all versions of the API.
72
+ #
73
+ # @example Get the summary representation.
74
+ # user_resource.summary("1.0.1")
75
+ #
76
+ # @param [ String ] version The version to get of the resource.
77
+ #
78
+ # @return [ Hash ] The summary representation of the resource.
79
+ #
80
+ # @since 0.0.0
81
+ def summary(version = nil)
82
+ representation(version, single(summary_attributes, resource, version))
83
+ end
84
+
85
+ private
86
+
87
+ def multiple(attributes, version, representation = [])
88
+ resource.each do |object|
89
+ representation.push(single(attributes, object, version))
90
+ end
91
+ representation
92
+ end
93
+
94
+ def representation(version, resource)
95
+ Registry.validate!(version)
96
+ Hash[ version: version || "none", resource: resource ]
97
+ end
98
+
99
+ def single(attributes, object, version, representation = {})
100
+ attributes.each do |name, attribute|
101
+ if attribute.displayable?(version)
102
+ representation[name] = attribute.value(object)
103
+ end
104
+ end
105
+ representation
106
+ end
107
+
108
+ class << self
109
+
110
+ # Including the module will inject the necessary macros into the base
111
+ # class.
112
+ #
113
+ # @exampe Include the Representable module.
114
+ # class UserResource
115
+ # include Darstellung::Representable
116
+ # end
117
+ #
118
+ # @param [ Class ] klass The class including the module.
119
+ #
120
+ # @since 0.0.0
121
+ def included(klass)
122
+ klass.extend(Macros)
123
+ end
124
+ end
125
+ end
126
+ end
@@ -1,4 +1,4 @@
1
1
  # encoding: utf-8
2
2
  module Darstellung
3
- VERSION = "0.0.0"
3
+ VERSION = "0.0.1"
4
4
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: darstellung
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.0
4
+ version: 0.0.1
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,16 +9,22 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-12-24 00:00:00.000000000 Z
12
+ date: 2012-12-25 00:00:00.000000000 Z
13
13
  dependencies: []
14
- description: Create API representations in Ruby
14
+ description: Simple and fast representations for APIs in Ruby
15
15
  email:
16
16
  - durran@gmail.com
17
17
  executables: []
18
18
  extensions: []
19
19
  extra_rdoc_files: []
20
20
  files:
21
+ - lib/darstellung/attribute.rb
22
+ - lib/darstellung/definable.rb
23
+ - lib/darstellung/macros.rb
24
+ - lib/darstellung/registry.rb
25
+ - lib/darstellung/representable.rb
21
26
  - lib/darstellung/version.rb
27
+ - lib/darstellung.rb
22
28
  - README.md
23
29
  - LICENSE
24
30
  - Rakefile
@@ -36,7 +42,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
36
42
  version: '0'
37
43
  segments:
38
44
  - 0
39
- hash: -1882674926009562910
45
+ hash: 220129650249248843
40
46
  required_rubygems_version: !ruby/object:Gem::Requirement
41
47
  none: false
42
48
  requirements:
@@ -45,11 +51,11 @@ required_rubygems_version: !ruby/object:Gem::Requirement
45
51
  version: '0'
46
52
  segments:
47
53
  - 0
48
- hash: -1882674926009562910
54
+ hash: 220129650249248843
49
55
  requirements: []
50
56
  rubyforge_project:
51
57
  rubygems_version: 1.8.24
52
58
  signing_key:
53
59
  specification_version: 3
54
- summary: Create API representations in Ruby
60
+ summary: Simple and fast representations for APIs in Ruby
55
61
  test_files: []