moonrope 1.4.1 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (95) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +9 -0
  3. data/Gemfile.lock +47 -0
  4. data/MIT-LICENCE +20 -0
  5. data/README.md +24 -0
  6. data/bin/moonrope +28 -0
  7. data/docs/authentication.md +114 -0
  8. data/docs/controllers.md +106 -0
  9. data/docs/exceptions.md +27 -0
  10. data/docs/introduction.md +29 -0
  11. data/docs/structures.md +214 -0
  12. data/example/authentication.rb +50 -0
  13. data/example/controllers/meta_controller.rb +14 -0
  14. data/example/controllers/users_controller.rb +92 -0
  15. data/example/structures/pet_structure.rb +12 -0
  16. data/example/structures/user_structure.rb +35 -0
  17. data/html/assets/lock.svg +3 -0
  18. data/html/assets/reset.css +101 -0
  19. data/html/assets/style.css +348 -0
  20. data/html/assets/tool.svg +4 -0
  21. data/html/assets/try.js +151 -0
  22. data/html/authenticators/default.html +191 -0
  23. data/html/controllers/meta/version.html +144 -0
  24. data/html/controllers/meta.html +73 -0
  25. data/html/controllers/users/create.html +341 -0
  26. data/html/controllers/users/list.html +348 -0
  27. data/html/controllers/users/show.html +261 -0
  28. data/html/controllers/users/update.html +387 -0
  29. data/html/controllers/users.html +93 -0
  30. data/html/index.html +166 -0
  31. data/html/moonrope.txt +0 -0
  32. data/html/structures/pet.html +176 -0
  33. data/html/structures/user.html +338 -0
  34. data/lib/moonrope/action.rb +165 -37
  35. data/lib/moonrope/authenticator.rb +39 -0
  36. data/lib/moonrope/base.rb +24 -6
  37. data/lib/moonrope/controller.rb +4 -2
  38. data/lib/moonrope/doc_context.rb +94 -0
  39. data/lib/moonrope/doc_server.rb +123 -0
  40. data/lib/moonrope/dsl/action_dsl.rb +159 -9
  41. data/lib/moonrope/dsl/authenticator_dsl.rb +31 -0
  42. data/lib/moonrope/dsl/base_dsl.rb +21 -18
  43. data/lib/moonrope/dsl/controller_dsl.rb +60 -9
  44. data/lib/moonrope/dsl/filterable_dsl.rb +27 -0
  45. data/lib/moonrope/dsl/structure_dsl.rb +27 -2
  46. data/lib/moonrope/errors.rb +3 -0
  47. data/lib/moonrope/eval_environment.rb +82 -3
  48. data/lib/moonrope/eval_helpers/filter_helper.rb +82 -0
  49. data/lib/moonrope/eval_helpers.rb +28 -5
  50. data/lib/moonrope/guard.rb +35 -0
  51. data/lib/moonrope/html_generator.rb +65 -0
  52. data/lib/moonrope/param_set.rb +11 -1
  53. data/lib/moonrope/rack_middleware.rb +1 -1
  54. data/lib/moonrope/railtie.rb +31 -14
  55. data/lib/moonrope/request.rb +25 -14
  56. data/lib/moonrope/structure.rb +74 -11
  57. data/lib/moonrope/structure_attribute.rb +15 -0
  58. data/lib/moonrope/version.rb +1 -1
  59. data/lib/moonrope.rb +5 -4
  60. data/moonrope.gemspec +21 -0
  61. data/spec/spec_helper.rb +32 -0
  62. data/spec/specs/action_spec.rb +455 -0
  63. data/spec/specs/base_spec.rb +29 -0
  64. data/spec/specs/controller_spec.rb +31 -0
  65. data/spec/specs/param_set_spec.rb +31 -0
  66. data/templates/basic/_action_form.erb +77 -0
  67. data/templates/basic/_errors_table.erb +32 -0
  68. data/templates/basic/_structure_attributes_list.erb +55 -0
  69. data/templates/basic/action.erb +168 -0
  70. data/templates/basic/assets/lock.svg +3 -0
  71. data/templates/basic/assets/reset.css +101 -0
  72. data/templates/basic/assets/style.css +348 -0
  73. data/templates/basic/assets/tool.svg +4 -0
  74. data/templates/basic/assets/try.js +151 -0
  75. data/templates/basic/authenticator.erb +51 -0
  76. data/templates/basic/controller.erb +20 -0
  77. data/templates/basic/index.erb +114 -0
  78. data/templates/basic/layout.erb +46 -0
  79. data/templates/basic/structure.erb +23 -0
  80. data/test/test_helper.rb +81 -0
  81. data/test/tests/action_access_test.rb +63 -0
  82. data/test/tests/actions_test.rb +524 -0
  83. data/test/tests/authenticators_test.rb +87 -0
  84. data/test/tests/base_test.rb +35 -0
  85. data/test/tests/controllers_test.rb +49 -0
  86. data/test/tests/eval_environment_test.rb +136 -0
  87. data/test/tests/evel_helpers_test.rb +60 -0
  88. data/test/tests/examples_test.rb +11 -0
  89. data/test/tests/helpers_test.rb +97 -0
  90. data/test/tests/param_set_test.rb +44 -0
  91. data/test/tests/rack_middleware_test.rb +109 -0
  92. data/test/tests/request_test.rb +232 -0
  93. data/test/tests/structures_param_extensions_test.rb +159 -0
  94. data/test/tests/structures_test.rb +335 -0
  95. metadata +82 -48
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: e8a0b6679bab153541587026c146967da03c4935
4
- data.tar.gz: 7ae7f36435dba370c40d91c122041cd8cf4799f7
3
+ metadata.gz: 5af89a4e614e26c289fad12a69304c7a2f293562
4
+ data.tar.gz: e2db44de7655b8d4d3aa659fd814b1fdca6af522
5
5
  SHA512:
6
- metadata.gz: b58205dfa858007e42eaf936561614184e229e56ba12033ff74d38a7ffcf5a70f4b53f0e1739ed6c1b575fa2e50e3b71b3424de7638d336be58ad5a31f5e80ce
7
- data.tar.gz: eb52281364f9c559b15222cdbf5a8af671eae34ebf5238529b1fdf8b7f6c9883f55c004a3dca803d5c1f0bce98c01360fdae1dd949d7128cacdb07b448758ae0
6
+ metadata.gz: 90e9f623a0f62e3564fc9ba17850684f263810a04c707ff11765d12522ceef810d8524f8c93b8cc9b3a29e6a72663593552fe6050016c57c7ffe373128969d0c
7
+ data.tar.gz: 1d661f3d8acc4352ecc936ccbbf42d18afbefcf128db5b993e0c3edd947b84b360af8b6864e21a974b12c29c44be50e04d233c83a46afe1cce7fbd6c62704e8f
data/Gemfile ADDED
@@ -0,0 +1,9 @@
1
+ source "https://rubygems.org"
2
+ gemspec
3
+ gem 'rspec'
4
+ gem 'rspec-core'
5
+ gem 'rspec-expectations'
6
+ gem 'rspec-mocks'
7
+ gem "test-unit", '~> 2.5'
8
+ gem 'yard', '~> 0.8'
9
+ gem "rack-test", '~> 0'
data/Gemfile.lock ADDED
@@ -0,0 +1,47 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ moonrope (2.0.0)
5
+ deep_merge (~> 1.0)
6
+ json (~> 1.7)
7
+ rack (>= 1.4)
8
+
9
+ GEM
10
+ remote: https://rubygems.org/
11
+ specs:
12
+ deep_merge (1.0.1)
13
+ diff-lcs (1.2.5)
14
+ json (1.8.3)
15
+ rack (1.5.2)
16
+ rack-test (0.6.3)
17
+ rack (>= 1.0)
18
+ rake (10.3.1)
19
+ rspec (0.9.4)
20
+ rspec-core (3.3.2)
21
+ rspec-support (~> 3.3.0)
22
+ rspec-expectations (3.3.1)
23
+ diff-lcs (>= 1.2.0, < 2.0)
24
+ rspec-support (~> 3.3.0)
25
+ rspec-mocks (3.3.2)
26
+ diff-lcs (>= 1.2.0, < 2.0)
27
+ rspec-support (~> 3.3.0)
28
+ rspec-support (3.3.0)
29
+ test-unit (2.5.5)
30
+ yard (0.8.7.2)
31
+
32
+ PLATFORMS
33
+ ruby
34
+
35
+ DEPENDENCIES
36
+ moonrope!
37
+ rack-test (~> 0)
38
+ rake (~> 10.3)
39
+ rspec
40
+ rspec-core
41
+ rspec-expectations
42
+ rspec-mocks
43
+ test-unit (~> 2.5)
44
+ yard (~> 0.8)
45
+
46
+ BUNDLED WITH
47
+ 1.10.6
data/MIT-LICENCE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2014 Adam Cooke.
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,24 @@
1
+ # Moonrope - the self-documenting API
2
+
3
+ Moonrope is an API server & client tool for Ruby/Rack applications. It
4
+ provides everything you need to create an API within your application and
5
+ have easy to use client libraries provided without any development.
6
+
7
+ Not only does it provide a formal structure for your API, it will also generate
8
+ lovely looking documentation files for develpers to work with.
9
+
10
+ This repository is the server-side library which allows you to easily define
11
+ your API actions & data structures and serve them out using Rack middleware.
12
+
13
+ * [Documentation](http://rdoc.info/github/viaduct/moonrope/master/frames)
14
+ * [Travis CI](https://travis-ci.org/viaduct/moonrope)
15
+
16
+ ![Screenshot](https://s.adamcooke.io/16/VJpLoJku.png)
17
+
18
+ ## Documentation
19
+
20
+ * [Introduction](https://github.com/adamcooke/moonrope/blob/master/docs/introduction.md)
21
+ * [Controllers & Actions](https://github.com/adamcooke/moonrope/blob/master/docs/controllers.md)
22
+ * [Structures](https://github.com/adamcooke/moonrope/blob/master/docs/structures.md)
23
+ * [Authentication](https://github.com/adamcooke/moonrope/blob/master/docs/authentication.md)
24
+ * [Exceptions](https://github.com/adamcooke/moonrope/blob/master/docs/exceptions.md)
data/bin/moonrope ADDED
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env ruby
2
+ $LOAD_PATH.unshift(File.expand_path('../../lib', __FILE__))
3
+ require 'moonrope'
4
+ require 'moonrope/html_generator'
5
+
6
+ if ARGV.size != 2
7
+ puts "usage: moonrope {path to api} {export dir}"
8
+ exit 1
9
+ end
10
+
11
+ api_root = ARGV[0]
12
+ export_dir = ARGV[1]
13
+ template_name = "basic"
14
+
15
+ unless File.directory?(api_root)
16
+ $stderr.puts "No directory found at #{api_root}"
17
+ exit 1
18
+ end
19
+
20
+ if File.exists?(export_dir) && Dir[File.join(export_dir, '*')].size > 0
21
+ $stderr.puts "File already exists and is not empty at #{export_dir}. Delete and try again."
22
+ exit 1
23
+ end
24
+
25
+ base = Moonrope::Base.load(ARGV[0])
26
+ generator = Moonrope::HtmlGenerator.new(base, File.expand_path("../../templates/#{template_name}", __FILE__))
27
+ generator.generate(export_dir)
28
+ puts "\e[32mMoonrope documentation generated and saved at #{export_dir}\e[0m"
@@ -0,0 +1,114 @@
1
+ # Authentication
2
+
3
+ Authentication consumers to your API is a key part of developing it. Moonrope's
4
+ authentication layer provides you with fine grained control of how your authentication
5
+ works while still maintaining documentability.
6
+
7
+ ## Getting Started
8
+
9
+ To begin, you need to define an `authenticator`. An authenticator's role is to
10
+ extract an "identity" from an API request. In this example, we'll be authenticating
11
+ our consumers using a token that will be unique to each user. The example below
12
+ demonstrates a very simple authenticator.
13
+
14
+ ```ruby
15
+ authenticator :default do
16
+
17
+ header "X-Auth-Token", "The user's unique API token.", :example => 'f29a45f0-b6da-44ae-a029-d4e1744ebaee'
18
+
19
+ error 'InvalidAPIToken', "The API token provided in X-Auth-Token was not valid.", :attributes => {:token => "The token that was looked up"}
20
+
21
+ lookup do
22
+ if token = request.headers['X-Auth-Token']
23
+ if user = User.find_by_api_token(token)
24
+ user
25
+ else
26
+ error 'InvalidAPIToken', :token => token
27
+ end
28
+ end
29
+ end
30
+
31
+ rule :default, "AccessDenied", "Must be authenticated as a user." do
32
+ identity.is_a?(User)
33
+ end
34
+
35
+ rule :anonymous, "MustBeAnonymous", "Must be anonymous." do
36
+ identity.nil?
37
+ end
38
+
39
+ end
40
+ ```
41
+
42
+ Let's break that down:
43
+
44
+ * The first line sets the name of the authenticator. In most cases you'll only
45
+ ever have one which should be named `:default`. This will apply to all actions
46
+ in your API.
47
+
48
+ * Next, we define a that the authenticator uses the `X-Auth-Token` header. We
49
+
50
+ provide a description and example for documentation purposes.
51
+ * Next, we define that a `InvalidAPIToken` error may be raised when trying to
52
+ lookup the identity for the request. We include a description plus a hash of
53
+ attributes that should be returned with the error.
54
+
55
+ * Next, we define a `lookup` block which specifies how to lookup your identity
56
+ object from the request. This is executed in the same scope that would be used
57
+ for any action in the API. This block will either return the identity object,
58
+ raise an error or return nothing. If it returns something, that will be used
59
+ as the identity object and the request will continue. If it raises an error,
60
+ the error will be returned to the user and the request will stop. It if returns
61
+ nothing, the request will continue however there will be no identity.
62
+
63
+ * Next, we set a default access rule which is executed on every request to
64
+ verify that the identity has access to the requested action. The `default` rule
65
+ will apply to all actions however you can create others which can be chosen
66
+ for specific actions or controllers. The block for this rule must return a true
67
+ or false value depending on whether the identity satisfies the access condition.
68
+ The second argument is the error code which will be returned if this condition
69
+ is not satified for the request. The third argument is the description of the
70
+ actual condition (for documentation).
71
+
72
+ * Finally, we define an anonymous rule which can be used for any actions where there
73
+ should not be any identity provided.
74
+
75
+ ## Choosing authenticators & access rules
76
+
77
+ When you create actions you can choose which authenticator & access rule should
78
+ be applied when the action is requested. It's probably easiest to demonstrate
79
+ this with some code:
80
+
81
+ ```ruby
82
+ controller :users do
83
+
84
+ action :list do
85
+ # By default, this action will use the 'default' authenticator and the
86
+ # 'default' access rule.
87
+ end
88
+
89
+ action :colors do
90
+ # This action will use the 'default' authenticator's anonymous rule.
91
+ access_rule :anonymous
92
+ end
93
+
94
+ action :create do
95
+ # This action will use the 'default' access rule on the 'two_factor
96
+ # authenticator.
97
+ authenticator :two_factor
98
+ end
99
+
100
+ action :destroy do
101
+ # This action will use the 'admin' access rule on the 'two_factor'
102
+ # authenticator.
103
+ access_rule :two_factor => :admin
104
+ end
105
+
106
+ end
107
+ ```
108
+
109
+ These different actions all use different rules & authenticators. The generated
110
+ documentation will reflect these as appropriate.
111
+
112
+ It's worth noting that the `authenticator` and `access_rule` methods which are
113
+ shown here on an action, can also be applied at the controller level and they'll
114
+ apply to any actions that don't override them.
@@ -0,0 +1,106 @@
1
+ # Controllers & Actions
2
+
3
+ Before you can create an action, you'll need to create a controller which can
4
+ contain your action. In this example, we're going to create an action which
5
+ will just say "Hello world!".
6
+
7
+ ```ruby
8
+ controller :hello do
9
+ action :world do
10
+ description "Say hello world"
11
+ action do
12
+ "Hello world!"
13
+ end
14
+ end
15
+ end
16
+ ```
17
+
18
+ This is the most basic definition of a controller & action. It specifies the
19
+ name of the controller (`hello`) and then adds an action to this controller.
20
+ This action has the name `world`, a description and a block which specifies
21
+ what you should be executed when this action is run.
22
+
23
+ ## Parameters
24
+
25
+ It's very common to need to receive additional information when the request
26
+ is made. Moonrope allows you to receive parameters into your action like
27
+ normal HTTP requests.
28
+
29
+ To access a paremters within your action (or helper), you can simply access
30
+ it using `params[:name_of_parameter]` or `params.name_of_parameter`. For example,
31
+ if you wanted to access a parameter named `page` you can use `params[:page]` or
32
+ `params.page`. If the value sent is nil or an empty string this will return nil.
33
+
34
+ ### Defining supported parameters
35
+
36
+ You can define which parameters are supported for actions. This is an example
37
+ action which defines some parameters.
38
+
39
+ ```ruby
40
+ action :say_hello do
41
+ description "Say some things to a user"
42
+ param :name, :required => true, :type => String
43
+ param :age, :required => true, :type => Integer
44
+ param :hair_color, :type => String, :default => 'Unknown'
45
+ param :phone_number, :type => String, :regex => /\A\+[\d\s]+\z/
46
+ action do
47
+ "Hello #{params.name}! You are #{params.age} and your hair is #{params.hair_color}!"
48
+ end
49
+ end
50
+ ```
51
+
52
+ When defining a parameter you can define a number of options to assist with
53
+ validation & default population.
54
+
55
+ * `:required => true` - this will require that this parameter is passed with
56
+ the request. If not an error will be raised before the action is executed.
57
+ * `:type => String` - this sets what type of object should be submitted. In
58
+ most cases this should be `String`, `Integer`, `Hash` or `Array`.
59
+ * `:default => 'Value here'` - sets the default value for the parameter if
60
+ none is passed.
61
+ * `:regex => //` - sets a regex which the passed value must conform to
62
+
63
+ ## Raising errors
64
+
65
+ If an action cannot be completed, you'll need to return an error. So that Moonrope
66
+ can document your API, you need to define all the types of error that an action
67
+ can return.
68
+
69
+ ```ruby
70
+ action :show do
71
+ error "ApplicationSuspended", "The application ({app_name}) you're trying to access is suspended.", :attributes => {:app_name => "The name of the application"}
72
+ action do
73
+ if application.suspended?
74
+ error "ApplicationSuspended", :app_name => application.name
75
+ end
76
+ end
77
+ end
78
+ ```
79
+
80
+ In this example, you're defining an error called `ApplicationSuspended` along
81
+ with some information about what it means plus an array of attributes which will
82
+ be returned with it. When you're actually in the action, you're going to call that
83
+ error by specifying the name and the attributes to provide. This will then be
84
+ returned to the user along with the previously defined message and attributes.
85
+
86
+ These will always be returned to the consumer with the `error` status.
87
+
88
+ ## Flags
89
+
90
+ Flags contain extra information which you wish to return with your request
91
+ without interrupting the data you are returning. Think of them like HTTP headers.
92
+ A use case for these may be that you wish to paginate data and need to return
93
+ pagination details along with the actual data.
94
+
95
+ ```ruby
96
+ action do
97
+ # Get some pagianted data
98
+ pages = Page.paginate(:page => params.page)
99
+ # Set the flags
100
+ set_flag :current_page, pages.page
101
+ set_flag :items_per_page, pages.items_per_page
102
+ set_flag :total_pages, pages.total_pages
103
+ # Return all the pages as normal
104
+ pages
105
+ end
106
+ ```
@@ -0,0 +1,27 @@
1
+ # Exceptions raised during requests
2
+
3
+ Moonrope will rescue any exceptions which are raised during a Rack request.
4
+ These will be logged to the Moonrope logger and a 500 error will be returned
5
+ to the client which made the bad request.
6
+
7
+ You can also register a callback to be notified whenever an exception is raised
8
+ in the request lifecycle. This will be useful if you want to report these
9
+ exceptions to an external exception reporting service.
10
+
11
+ This is configured by registering the callback with the Moonrope::Base instance.
12
+ In a Rails application, this can be added as follows.
13
+
14
+ ```ruby
15
+ Rails.application.config.moonrope.register_request_error_callback do |request, error|
16
+ tags = {
17
+ 'component' => 'API'
18
+ }
19
+ context = {
20
+ 'controller' => request.controller.try(:name).to_s,
21
+ 'action' => request.action.try(:name).to_s,
22
+ 'identity' => request.identity.is_a?(ActiveRecord::Base) ? "#{request.identity.class}##{request.identity.id}" : request.identity.to_s,
23
+ 'params' => request.params._as_hash
24
+ }
25
+ Raven.capture_exception(error, :tags => tags, :extra => context)
26
+ end
27
+ ```
@@ -0,0 +1,29 @@
1
+ # The key components of a Moonrope API
2
+
3
+ * **Controller** - a controller is a set of actions which can be executed by
4
+ users.
5
+
6
+ * **Action** - an action is a method which someone can execute on your API. An
7
+ action may return data, update data or destroy data. Every action returns
8
+ some data to a user.
9
+
10
+ * **Structure** - a structure allows you to convert a Ruby object (in most
11
+ cases this would be an Active Record model) into a hash which can be returned
12
+ to a user.
13
+
14
+ * **Helper** - a helper is a method which you can define globally or on a
15
+ per controller basis. A helper can execute code & return objects which you
16
+ can use in your actions.
17
+
18
+ These various components are defined in files which are then loaded by
19
+ Moonrope automatically when your application starts. In a Rails application,
20
+ by default these should be placed into a `api` directory in the root of
21
+ your application. The actual directory structure for your Moonrope API should
22
+ look like this:
23
+
24
+ * `api/controllers` - contains all of your controllers
25
+ * `api/structures` - contains all your structures
26
+ * `api/helpers` - contains any helpers you have defined
27
+
28
+ The name of files within these folders is not important. All files ending with
29
+ `.rb` are loaded.
@@ -0,0 +1,214 @@
1
+ # Structures
2
+
3
+ A structure is a blueprint outlining out an object can be converted into a hash
4
+ for output in your API.
5
+
6
+ Say, for example, you have a User object and you wish to present this over your
7
+ API. You would find your user and then pass this through your `user` structure
8
+ which will return an appropriate hash based on the context the structure is
9
+ being called from and your authenticated object.
10
+
11
+ ## Creating a structure
12
+
13
+ Structures should be placed into your `structures` directory and best practice
14
+ dictates they should simply be named `object_structure.rb`, for example a user structure
15
+ would be named `user_structure.rb`.
16
+
17
+ ```ruby
18
+ structure :user do
19
+
20
+ basic :id, :type => Integer, :example => 1234
21
+ basic :username, :type => String, :example => "adam"
22
+
23
+ full :full_name, :type => String, :example => "Adam"
24
+ full :last_name, :type => String, :example => "Cooke"
25
+ full :age, :type => Integer, :example => 27
26
+ full :created_at, :type => String, :example => "2014-07-01T11:32:59+01:00"
27
+ full :updated_at, :type => String, :example => "2014-07-01T11:32:59+01:00"
28
+
29
+ end
30
+ ```
31
+
32
+ This example is the most basic way of defining a structure. You see we have defined
33
+ a number of attributes which should be included in our structure. Basic attributes are
34
+ always included in the structure whereas full attributes are only included when requested.
35
+
36
+ The basic attributes from a structure is often used on its own when referenced
37
+ from other structures. For example, if users had many projects, the project
38
+ structure may reference user but only request the basic information as any further
39
+ information is not needed.
40
+
41
+ The full attributes would be returned when listing a specific user or a list
42
+ of users on their own. For example, your users/list or users/info methods would
43
+ likely return full information rather than just the basic.
44
+
45
+ Note that when full information is requested, it is always combined with the
46
+ information from basic so there's no need to duplicate attribute definitions.
47
+
48
+ ### Mapping
49
+
50
+ Any attributes which you add to your structure are mapped one to one with the attributes
51
+ available on the source object. If the name doesn't match, you can use the `:name`
52
+ option to set the actual name of the attribute on the source object.
53
+
54
+ ```ruby
55
+ basic :user_id, :source_attribute => :id
56
+ ```
57
+
58
+ Alternatively, you can specify a block which will be used when mapping the value to the
59
+ correct object.
60
+
61
+ ```ruby
62
+ basic :name_in_caps, :value => Proc.new { o.name.upcase }
63
+ ```
64
+
65
+ ### Grouping
66
+
67
+ Attributes can be placed into groups which will return a hash containing all items
68
+ within the group.
69
+
70
+ ```ruby
71
+ group :financials do
72
+ basic :balance, :type => Integer, :example => 12345
73
+ full :last_invoice_raised_at, :type => String, :example => ""
74
+ end
75
+ ```
76
+
77
+ ### Expansions
78
+
79
+ An expansion allows you to manually define extra information which can be
80
+ returned with your user object. For example, in some API methods you may wish
81
+ to return extra information about the user's current financial status which
82
+ isn't usually returned.
83
+
84
+ In most cases, an expansion will be a link to another structure which you have
85
+ defined. An expansion can be defined as shown below:
86
+
87
+ ```ruby
88
+ # A single object which is associated with your user (belongs to)
89
+ expansion :currency, :type => Hash, :structure => :currency
90
+ # or including an array of objects (has many)
91
+ expansion :projects, :type => Array, :structure => :project
92
+ ```
93
+
94
+ The same `:structure => :name` can be used on any attribute which you define in your
95
+ structure. Therefore, if you need to always include a structure, you can simply
96
+ add it to a full or basic line.
97
+
98
+ ### Conditional attributes
99
+
100
+ You can specify a condition on any attribute or expansion. This can be done by passing
101
+ a block to the `:if` option when defining an attribute.
102
+
103
+ ```ruby
104
+ condition Proc.new { identity.is_super_special_admin? }, "Only available to special admins" do
105
+ basic :pin
106
+ end
107
+ ```
108
+
109
+ As well as specifying inline blocks, you can also reference access rules from any
110
+ of your authenticators.
111
+
112
+ ```ruby
113
+ condition :default => :admins do
114
+ basic :pin
115
+ end
116
+ ```
117
+
118
+ This will use the `admins` rule on the `default` authenticator to verify whether
119
+ or not the `pin` attribute will be displayed.
120
+
121
+ If you're only using your default authenticator, you can omit the authenticator
122
+ name in this call. For example:
123
+
124
+ ```ruby
125
+ condition :admins do
126
+ basic :pin
127
+ end
128
+ ```
129
+
130
+ ## Accessing structures from actions
131
+
132
+ Now... how do you include structures from within an action I hear you ask.
133
+ That's actually pretty simple. Throughout both actions and structures, you can
134
+ use the `structure` method to load a structure's hash. Here are a number of
135
+ examples which you can use to load a hash from a structure.
136
+
137
+ ```ruby
138
+ # Return the basic information for a user
139
+ structure(:user, user)
140
+
141
+ # Return the full information for a user
142
+ structure(:user, user, :full => true)
143
+
144
+ # Return the full information plus all expansions
145
+ structure(:user, user, :full => true, :expansions => true)
146
+
147
+ # Return the full information plus specified expansions
148
+ structure(:user, user, :full => true, :expansions => [:projects, :financials])
149
+
150
+ # You don't need to provide the name of the structure if it can
151
+ # be auto-determined from the name of the class.
152
+ structure(user, :full => true)
153
+ ```
154
+
155
+ Remember, these can be used in structures as well as actions. So, you may
156
+ want to create expansions which link to other structures. For example, if you
157
+ wanted your user hash to include a list of associated projects:
158
+
159
+ ```ruby
160
+ expansion :projects do
161
+ o.projects.map { |p| structure(:project, p) }
162
+ end
163
+ ```
164
+
165
+ If you wish to check whether or not a structure exists before calling it from
166
+ an action, you can use the `has_structure_for?` method as shown below.
167
+
168
+ ```ruby
169
+ if has_structure_for?(:user)
170
+ structure(:user, user)
171
+ else
172
+ error :error, "Structure not found."
173
+ end
174
+ ```
175
+
176
+ In some cases, you may wish for your consumers to decide which expansions
177
+ should be returned. This can be achieved by passing `:paramable` when calling
178
+ a `structure` from an action. For example:
179
+
180
+ ```ruby
181
+ # Allow consumer to choose from any expansion registered on the structure.
182
+ # (taking into account any conditions you specify) and choose whether or not
183
+ # to include `full` attributes.
184
+ structure(user, :paramable => true)
185
+
186
+ # Allow the consumer to choose any expansions but not allow control over whether
187
+ # full attributes should be returned. Returns all expansions by default.
188
+ structure(user, :paramable => {:expansions => true})
189
+
190
+ # Allow the consumer to choose any expansions but not allow control over whether
191
+ # full attributes should be returned. Returns no expansions by default.
192
+ structure(user, :paramable => {:expansions => false})
193
+
194
+ # Allow consumer to choose from any expansions you list. By default, the items
195
+ # you list will be included in the response if the user does not request any
196
+ # specific expansions.
197
+ structure(user, :paramable => {:expansions => [:server, :endpoint]})
198
+
199
+ # Allow the consumer to control whether or not full attributes should be returned
200
+ # in the structure or not but do not allow any control of expansions.
201
+ structure(user, :paramable => {:full => true})
202
+
203
+ # Allow the consumer to control whether or not full attributes should be returned
204
+ # but do not return them by default.
205
+ structure(user, :paramable => {:full => false})
206
+ ```
207
+
208
+ To access these, consumers should send an `_expansions` param with their request which
209
+ should contain an array containing the names of the expansions that should be
210
+ included. Consumers can also send `true` or `false` in this parameter to return
211
+ all or no expansions.
212
+
213
+ To control access to `full` attributes, consumers should pass `_full` parameter
214
+ as true or false.