darstellung 0.0.0 → 0.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/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: []