moonrope 1.4.1 → 2.0.0

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.
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.