her 0.5.3 → 0.5.4

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 (72) hide show
  1. checksums.yaml +8 -8
  2. data/.gitignore +2 -2
  3. data/.rspec +1 -2
  4. data/.travis.yml +2 -2
  5. data/README.md +10 -16
  6. data/UPGRADE.md +4 -0
  7. data/examples/grape-and-her/.env.default +3 -0
  8. data/examples/grape-and-her/Procfile +2 -0
  9. data/examples/grape-and-her/README.md +27 -0
  10. data/examples/grape-and-her/api/Gemfile +11 -0
  11. data/examples/grape-and-her/api/Rakefile +14 -0
  12. data/examples/grape-and-her/api/app/api.rb +49 -0
  13. data/examples/grape-and-her/api/app/models/organization.rb +7 -0
  14. data/examples/grape-and-her/api/app/models/user.rb +9 -0
  15. data/examples/grape-and-her/api/app/views/organizations/_base.rabl +2 -0
  16. data/examples/grape-and-her/api/app/views/organizations/index.rabl +3 -0
  17. data/examples/grape-and-her/api/app/views/organizations/show.rabl +3 -0
  18. data/examples/grape-and-her/api/app/views/users/_base.rabl +8 -0
  19. data/examples/grape-and-her/api/app/views/users/index.rabl +3 -0
  20. data/examples/grape-and-her/api/app/views/users/show.rabl +3 -0
  21. data/examples/grape-and-her/api/config.ru +5 -0
  22. data/examples/grape-and-her/api/config/boot.rb +17 -0
  23. data/examples/grape-and-her/api/config/unicorn.rb +7 -0
  24. data/examples/grape-and-her/api/db/migrations/001_create_users.rb +11 -0
  25. data/examples/grape-and-her/api/db/migrations/002_create_organizations.rb +8 -0
  26. data/examples/grape-and-her/consumer/Gemfile +23 -0
  27. data/examples/grape-and-her/consumer/app/assets/stylesheets/application.scss +190 -0
  28. data/examples/grape-and-her/consumer/app/assets/stylesheets/reset.scss +53 -0
  29. data/examples/grape-and-her/consumer/app/consumer.rb +74 -0
  30. data/examples/grape-and-her/consumer/app/models/organization.rb +13 -0
  31. data/examples/grape-and-her/consumer/app/models/user.rb +13 -0
  32. data/examples/grape-and-her/consumer/app/views/index.haml +9 -0
  33. data/examples/grape-and-her/consumer/app/views/layout.haml +20 -0
  34. data/examples/grape-and-her/consumer/app/views/organizations/index.haml +25 -0
  35. data/examples/grape-and-her/consumer/app/views/organizations/show.haml +11 -0
  36. data/examples/grape-and-her/consumer/app/views/users/index.haml +33 -0
  37. data/examples/grape-and-her/consumer/app/views/users/show.haml +9 -0
  38. data/examples/grape-and-her/consumer/config.ru +20 -0
  39. data/examples/grape-and-her/consumer/config/boot.rb +30 -0
  40. data/examples/grape-and-her/consumer/config/unicorn.rb +7 -0
  41. data/examples/grape-and-her/consumer/lib/response_logger.rb +18 -0
  42. data/her.gemspec +2 -2
  43. data/lib/her/model.rb +22 -26
  44. data/lib/her/model/associations.rb +19 -19
  45. data/lib/her/model/attributes.rb +173 -0
  46. data/lib/her/model/base.rb +17 -0
  47. data/lib/her/model/http.rb +58 -242
  48. data/lib/her/model/introspection.rb +7 -8
  49. data/lib/her/model/nested_attributes.rb +3 -3
  50. data/lib/her/model/orm.rb +15 -205
  51. data/lib/her/model/parse.rb +86 -0
  52. data/lib/her/model/paths.rb +54 -14
  53. data/lib/her/version.rb +1 -1
  54. data/spec/model/attributes_spec.rb +139 -0
  55. data/spec/model/dirty_spec.rb +40 -0
  56. data/spec/model/introspection_spec.rb +5 -5
  57. data/spec/model/orm_spec.rb +14 -128
  58. data/spec/model/paths_spec.rb +26 -0
  59. data/spec/model/validations_spec.rb +17 -0
  60. data/spec/spec_helper.rb +7 -32
  61. data/spec/support/extensions/array.rb +5 -0
  62. data/spec/support/extensions/hash.rb +5 -0
  63. data/spec/support/macros/model_macros.rb +29 -0
  64. metadata +52 -15
  65. data/examples/twitter-oauth/Gemfile +0 -13
  66. data/examples/twitter-oauth/app.rb +0 -50
  67. data/examples/twitter-oauth/config.ru +0 -5
  68. data/examples/twitter-oauth/views/index.haml +0 -9
  69. data/examples/twitter-search/Gemfile +0 -12
  70. data/examples/twitter-search/app.rb +0 -55
  71. data/examples/twitter-search/config.ru +0 -5
  72. data/examples/twitter-search/views/index.haml +0 -9
checksums.yaml CHANGED
@@ -1,15 +1,15 @@
1
1
  ---
2
2
  !binary "U0hBMQ==":
3
3
  metadata.gz: !binary |-
4
- MmIxMjIyZWU2NTIyOTdmZDdlNzU2MjBiM2NlNDU2MmFhMjJlNzVhNQ==
4
+ MmJlNGEzMjNhZjMyOTYyZWVkYmM2NjhhNGEwZDAwZmY0ODk4MWRkNQ==
5
5
  data.tar.gz: !binary |-
6
- MDJmNTM0YzI1ZmY1OTM3NDM4NTczNDYxNWZjOWI0NjU1NzNlYzY2Mg==
6
+ OGVjNGUzNDY5YTQxZmQzMzgxMDJmMWM4OGUxNmRhMjE1NWY2YzQ3Mw==
7
7
  !binary "U0hBNTEy":
8
8
  metadata.gz: !binary |-
9
- M2Q3MDc2NWJmNDI1NDM3Y2M1YjNkNjgwN2JlMWY3M2M5ZjhmYWZmMTY4Njdk
10
- MzViMjE0NDY1ZDlmOTNkNzBkNWI1MzgwNWFmYWYyM2YzOWE3YTE5YjcyZTg4
11
- YzM5ZWZkMDFjODBjMjBlN2Q0MDBiNDI5MzYyYWNhODZlZDQzNDU=
9
+ YmQ0Y2VkYTM2YTg1ZmRkNzBmODkxMGVhMDI3NTI2YzRkZmYwZTJjMzE5OTI3
10
+ Nzg0ZjRiMzU3ZTEzMTY5YTBiMjk4YjYzYzYzMWMwNjFlMmU2MzI3MTdhZDQ5
11
+ OGUyODNhMTc4YTA3ZDYzYjVlOTc5ZGY1Y2UzZTk4MzJlM2U0YzI=
12
12
  data.tar.gz: !binary |-
13
- YWJmZDEwMzlhMzkxZWJlMDZjOTZlZDRkNzM4MTMyZDBlZTlkZmM1NWUzOTMz
14
- YWFkMjBlNjgzY2U2ZWQxYTg3ZThlODZkMGVkOWU3MzVjNTU2ODc5ZjYyMGQ2
15
- YzJkNWFhYjRiY2EzZGI1YzE1OGQ5YzJkYzVjYTY0MWJiYjBmNmQ=
13
+ NzVmNzU1OWVlNjQ0NzNlNWM2MmVjZDc2NjU5OWQ5NWJlN2I0ZWRkZmY3Y2Q4
14
+ MTI1YzkyMDJhNDM2ZDBmN2ZmNmE4MzQxODc5MzhmZDEwNGFjOTE4MzFmYzA4
15
+ OTY3OGZlNjM5MDMxZGFhMzExZDRjZTRmOTNkMjZkYWFkOTA4Nzc=
data/.gitignore CHANGED
@@ -2,7 +2,7 @@
2
2
  .bundle
3
3
  Gemfile.lock
4
4
  pkg/*
5
- .yardoc
6
- doc
7
5
  rake
8
6
  tmp
7
+ .env
8
+ *.db
data/.rspec CHANGED
@@ -1,2 +1 @@
1
- --color
2
- --format=documentation
1
+ --colour --format=Fivemat
data/.travis.yml CHANGED
@@ -3,6 +3,6 @@ language: ruby
3
3
  rvm:
4
4
  - 1.9.3
5
5
  - 1.9.2
6
- - 1.8.7
6
+ - 2.0.0
7
7
 
8
- script: "bundle exec rake spec"
8
+ script: "echo 'COME ON!' && bundle exec rake spec"
data/README.md CHANGED
@@ -92,7 +92,7 @@ user = User.new(:fullname => "Maeby Fünke")
92
92
  user.save
93
93
  ```
94
94
 
95
- You can look into the `examples` directory for sample applications using Her. For a complete reference of all the methods you can use, check out [the documentation](http://rdoc.info/github/remiprev/her).
95
+ You can look into the `examples` directory for a sample application using Her. For a complete reference of all the methods you can use, check out [the documentation](http://rdoc.info/github/remiprev/her).
96
96
 
97
97
  ## Middleware
98
98
 
@@ -102,31 +102,23 @@ Since Her relies on [Faraday](https://github.com/technoweenie/faraday) to send H
102
102
 
103
103
  Her doesn’t support authentication by default. However, it’s easy to implement one with request middleware. Using the `connection` block, we can add it to the middleware stack.
104
104
 
105
- For example, to add a API token header to your requests in a Rails application, you would do something like this:
105
+ For example, to add a token header to your API requests in a Rails application, you could use the excellent [`request_store`](https://rubygems.org/gems/request_store) gem like this:
106
106
 
107
107
  ```ruby
108
108
  # app/controllers/application_controller.rb
109
109
  class ApplicationController < ActionController::Base
110
- around_filter :do_with_authenticated_user
111
-
112
- def do_with_authenticated_user
113
- Thread.current[:my_api_token] = session[:my_api_token]
114
- begin
115
- yield
116
- ensure
117
- Thread.current[:my_access_token] = nil
118
- end
110
+ before_filter :set_user_api_token
111
+
112
+ protected
113
+ def set_user_api_token
114
+ RequestStore.store[:my_api_token] = current_user.api_token # or something similar based on `session`
119
115
  end
120
116
  end
121
117
 
122
118
  # lib/my_token_authentication.rb
123
119
  class MyTokenAuthentication < Faraday::Middleware
124
- def initialize(app, options={})
125
- @app = app
126
- end
127
-
128
120
  def call(env)
129
- env[:request_headers]["X-API-Token"] = Thread.current[:my_api_token] if Thread.current[:my_api_token].present?
121
+ env[:request_headers]["X-API-Token"] = RequestStore.store[:my_api_token]
130
122
  @app.call(env)
131
123
  end
132
124
  end
@@ -136,6 +128,7 @@ require "lib/my_token_authentication"
136
128
 
137
129
  Her::API.setup :url => "https://api.example.com" do |connection|
138
130
  connection.use MyTokenAuthentication
131
+ connection.use Faraday::Request::UrlEncoded
139
132
  connection.use Her::Middleware::DefaultParseJSON
140
133
  connection.use Faraday::Adapter::NetHttp
141
134
  end
@@ -781,6 +774,7 @@ These fine folks helped with Her:
781
774
  * [@joanniclaborde](https://github.com/joanniclaborde)
782
775
  * [@seanreads](https://github.com/seanreads)
783
776
  * [@jonkarna](https://github.com/jonkarna)
777
+ * [@aclevy](https://github.com/aclevy)
784
778
 
785
779
  ## License
786
780
 
data/UPGRADE.md CHANGED
@@ -2,6 +2,10 @@
2
2
 
3
3
  Here is a list of backward-incompatible changes that were introduced while Her is pre-1.0. After reaching 1.0, it will follow the [Semantic Versioning](http://semver.org/) system.
4
4
 
5
+ ## 0.5.4
6
+
7
+ * Her does not support Ruby 1.8.7 anymore. You should upgrade to 1.9.2, 1.9.3 or 2.0.0.
8
+
5
9
  ## 0.5
6
10
 
7
11
  * Her is now compatible with `ActiveModel` and includes `ActiveModel::Validations`.
@@ -0,0 +1,3 @@
1
+ RACK_ENV=development
2
+ API_PORT=3100
3
+ CONSUMER_PORT=3200
@@ -0,0 +1,2 @@
1
+ api: ruby -C ./api -S bundle exec unicorn -p $API_PORT -c ./config/unicorn.rb -E $RACK_ENV
2
+ consumer: ruby -C ./consumer -S bundle exec unicorn -p $CONSUMER_PORT -c ./config/unicorn.rb -E $RACK_ENV
@@ -0,0 +1,27 @@
1
+ # Grape + Her example
2
+
3
+ This is an example of how to use Her to consume a simple API. It consists of two separate applications, a REST API (powered by `grape` and `activerecord`) and a consumer application (powered by `sinatra` and `her`).
4
+
5
+ ![](http://i.imgur.com/AGfYwzl.png)
6
+
7
+ ## Installation and Usage
8
+
9
+ ```shell
10
+ # Clone the repository
11
+ $ git clone git://github.com/remiprev/her.git
12
+
13
+ # Go to the example directory
14
+ $ cd her/examples/grape-and-her
15
+
16
+ # Go to each application and run `bundle install`
17
+ $ cd api; bundle install; cd ..
18
+ $ cd consumer; bundle install; cd ..
19
+
20
+ # Create the API database
21
+ $ cd api; sqlite3 db/development.db ""; bundle exec rake db:migrate; cd ..
22
+
23
+ # Start foreman with the Procfile
24
+ $ foreman start
25
+ ```
26
+
27
+ This should start the API on `http://0.0.0.0:3100` and the consumer on `http://0.0.0.0:3200`
@@ -0,0 +1,11 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Server
4
+ gem 'unicorn'
5
+
6
+ # App
7
+ gem 'grape'
8
+ gem 'sqlite3'
9
+ gem 'activerecord', '~> 3.2'
10
+ gem 'portugal'
11
+ gem 'grape-rabl'
@@ -0,0 +1,14 @@
1
+ require 'portugal/tasks'
2
+ Portugal.configure do |config|
3
+ config.migrations_path = File.expand_path("../db/migrations", __FILE__)
4
+ end
5
+
6
+ # This task is called before Portugal executes its tasks.
7
+ # It should require ActiveRecord somehow and establish the database connection
8
+ #
9
+ # Change it so it works for your application
10
+ task :environment do
11
+ require 'bundler'
12
+ Bundler.require
13
+ ActiveRecord::Base.establish_connection adapter: 'sqlite3', database: File.expand_path('../db/development.db', __FILE__)
14
+ end
@@ -0,0 +1,49 @@
1
+ class API < Grape::API
2
+ content_type :json, "application/json; charset=UTF-8"
3
+ format :json
4
+ formatter :json, Grape::Formatter::Rabl
5
+ use(Rack::Config) { |env| env['api.tilt.root'] = File.expand_path('../views', __FILE__) }
6
+ rescue_from :all
7
+
8
+ resources :users do
9
+ desc 'Return all users'
10
+ get nil, :rabl => "users/index" do
11
+ @users = User.all
12
+ end
13
+
14
+ desc 'Return a specific user'
15
+ get ':id', :rabl => "users/show" do
16
+ @user = User.find(params[:id])
17
+ end
18
+
19
+ desc 'Create a new user'
20
+ post nil, :rabl => "users/show" do
21
+ @user = User.new(params[:user])
22
+ error!({ :errors => @user.errors.full_messages }, 400) unless @user.save
23
+ end
24
+ end
25
+
26
+ resources :organizations do
27
+ desc 'Return all organizations'
28
+ get nil, :rabl => "organizations/index" do
29
+ @organizations = Organization.all
30
+ end
31
+
32
+ desc 'Return a specific organization'
33
+ get ':id', :rabl => "organizations/show" do
34
+ @organization = Organization.find(params[:id])
35
+ end
36
+
37
+ desc 'Return all users for specific organization'
38
+ get ':id/users', :rabl => "users/index" do
39
+ organization = Organization.find(params[:id])
40
+ @users = organization.users
41
+ end
42
+
43
+ desc 'Create a new organization'
44
+ post nil, :rabl => "organizations/show" do
45
+ @organization = Organization.new(params[:organization])
46
+ error!({ :errors => @organization.errors.full_messages }, 400) unless @organization.save
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,7 @@
1
+ class Organization < ActiveRecord::Base
2
+ # Associations
3
+ has_many :users
4
+
5
+ # Validations
6
+ validates :name, presence: true
7
+ end
@@ -0,0 +1,9 @@
1
+ class User < ActiveRecord::Base
2
+ # Associations
3
+ belongs_to :organization
4
+
5
+ # Validations
6
+ validates :email, presence: true
7
+ validates :fullname, presence: true
8
+ validates :organization, presence: true
9
+ end
@@ -0,0 +1,2 @@
1
+ attribute :id
2
+ attribute :name
@@ -0,0 +1,3 @@
1
+ collection @organizations
2
+
3
+ extends("organizations/base")
@@ -0,0 +1,3 @@
1
+ object @organization
2
+
3
+ extends("organizations/base")
@@ -0,0 +1,8 @@
1
+ attribute :id
2
+ attribute :email
3
+ attribute :fullname
4
+ attribute :organization_id
5
+
6
+ child :organization do
7
+ extends("organizations/base")
8
+ end
@@ -0,0 +1,3 @@
1
+ collection @users
2
+
3
+ extends("users/base")
@@ -0,0 +1,3 @@
1
+ object @user
2
+
3
+ extends("users/base")
@@ -0,0 +1,5 @@
1
+ require File.expand_path('../config/boot', __FILE__)
2
+
3
+ map "/" do
4
+ run API
5
+ end
@@ -0,0 +1,17 @@
1
+ # Bundler setup
2
+ ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
3
+ require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE'])
4
+ Bundler.require(:default, ENV['RACK_ENV']) if defined? Bundler
5
+
6
+ # Configure ActiveRecord
7
+ ActiveRecord::Base.establish_connection adapter: 'sqlite3', database: File.expand_path('../../db/development.db', __FILE__)
8
+
9
+ # Require models
10
+ Dir[File.expand_path('../../app/models/**/*.rb', __FILE__)].each do |file|
11
+ dirname = File.dirname(file)
12
+ file_basename = File.basename(file, File.extname(file))
13
+ require "#{dirname}/#{file_basename}"
14
+ end
15
+
16
+ # Application setup
17
+ require File.expand_path('../../app/api', __FILE__)
@@ -0,0 +1,7 @@
1
+ if ENV["RACK_ENV"] == "development"
2
+ worker_processes 1
3
+ else
4
+ worker_processes 4
5
+ end
6
+
7
+ timeout 30
@@ -0,0 +1,11 @@
1
+ class CreateUsers < ActiveRecord::Migration
2
+ def change
3
+ create_table :users do |t|
4
+ t.string :email
5
+ t.string :fullname
6
+ t.integer :organization_id, null: false
7
+
8
+ t.timestamps
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,8 @@
1
+ class CreateOrganizations < ActiveRecord::Migration
2
+ def change
3
+ create_table :organizations do |t|
4
+ t.string :name
5
+ t.timestamps
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,23 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Server
4
+ gem 'unicorn'
5
+
6
+ # Web
7
+ gem 'sinatra'
8
+ gem 'sinatra-reloader'
9
+ gem 'haml'
10
+ gem 'her'
11
+ gem 'multi_json'
12
+
13
+ # Assets
14
+ gem 'sass'
15
+ gem 'sprockets'
16
+ gem 'sprockets-sass'
17
+ gem 'sprockets-helpers'
18
+
19
+ group :development do
20
+ gem 'pry'
21
+ gem 'rake'
22
+ gem 'foreman'
23
+ end
@@ -0,0 +1,190 @@
1
+ @import "reset.css";
2
+
3
+ * {
4
+ -webkit-box-sizing: border-box;
5
+ -moz-box-sizing: border-box;
6
+ box-sizing: border-box;
7
+ }
8
+
9
+ html {
10
+ font-size: 62.5%;
11
+ }
12
+
13
+ body {
14
+ background: #eee;
15
+ color: #000;
16
+ padding: 30px;
17
+ font-family: 'Helvetica Neue', Helvetica, sans-serif;
18
+ font-size: 150%;
19
+ }
20
+
21
+ input, textarea, select, option, button {
22
+ font-family: 'Helvetica Neue', Helvetica, sans-serif;
23
+ font-size: 100%;
24
+ }
25
+
26
+ hr {
27
+ display: none;
28
+ }
29
+
30
+ a {
31
+ color: #1b49ff;
32
+ }
33
+
34
+ #wrap {
35
+ background: #fff;
36
+ box-shadow: 0 0 20px rgba(0,0,0,0.3);
37
+ border: 1px solid #bbb;
38
+ border-radius: 4px;
39
+ overflow:hidden;
40
+ }
41
+
42
+ #main-header {
43
+ font-size: 130%;
44
+ background: #ddd;
45
+
46
+ strong {
47
+ font-family: Pacifico, serif;
48
+ font-size: 250%;
49
+ font-weight: normal;
50
+ display: inline-block;
51
+ vertical-align: middle;
52
+ }
53
+
54
+ span {
55
+ display: inline-block;
56
+ vertical-align: middle;
57
+ color: rgba(0,0,0,0.4);
58
+ font-weight: normal;
59
+ margin: 0 0 0 20px;
60
+ font-size: 80%;
61
+ }
62
+
63
+ a {
64
+ display: block;
65
+ padding: 30px 20px;
66
+ color: #000;
67
+ text-decoration: none;
68
+
69
+ &:hover {
70
+ background: rgba(0,0,0,0.05);
71
+ }
72
+ }
73
+ }
74
+
75
+ #main-footer {
76
+ pre {
77
+ padding: 20px;
78
+ color: rgba(0,0,0,0.75);
79
+ line-height: 1.4;
80
+ font-size: 80%;
81
+ }
82
+ }
83
+
84
+ #content {
85
+ padding: 30px;
86
+
87
+ h1 {
88
+ margin: 25px 0 15px;
89
+ font-size: 150%;
90
+ &:first-child { margin-top: 0; }
91
+ }
92
+
93
+ h2 {
94
+ margin: 25px 0 15px;
95
+ font-size: 120%;
96
+ &:first-child { margin-top: 0; }
97
+ }
98
+
99
+ h3 {
100
+ margin: 25px 0 15px;
101
+ font-size: 110%;
102
+ &:first-child { margin-top: 0; }
103
+ }
104
+
105
+ pre {
106
+ background: #f9ffeb;
107
+ border: 1px solid rgba(0,0,0,0.15);
108
+ padding: 15px;
109
+ }
110
+
111
+ form {
112
+ margin: 0 0 15px;
113
+ padding: 0 0 15px;
114
+ border-bottom: 1px solid #ddd;
115
+
116
+ p {
117
+ margin: 0 0 15px;
118
+ }
119
+
120
+ label {
121
+ display: block;
122
+ margin: 0 0 6px;
123
+ cursor: pointer;
124
+ }
125
+
126
+ input[type=text], input[type=email] {
127
+ padding: 10px;
128
+ width: 100%;
129
+ border: 1px solid #ccc;
130
+ border-radius: 2px;
131
+ }
132
+
133
+ button {
134
+ cursor: pointer;
135
+ background: #1b49ff;
136
+ color: #fff;
137
+ padding: 8px 11px;
138
+ border-radius: 3px;
139
+ border: 1px solid rgba(0,0,0,0.3);
140
+ }
141
+
142
+ .errors {
143
+ padding: 15px;
144
+ background: #fcc;
145
+ border: 1px solid rgba(0,0,0,0.2);
146
+ margin: 0 0 20px;
147
+ font-size: 90%;
148
+ color: #900;
149
+
150
+ li {
151
+ margin: 0 0 5px 20px;
152
+ list-style: disc;
153
+ &:last-child { margin-bottom: 0; }
154
+ }
155
+ }
156
+ }
157
+
158
+ .records {
159
+ li {
160
+ list-style: disc;
161
+ margin: 0 0 10px 20px;
162
+
163
+ &:last-child { margin-bottom: 0; }
164
+ }
165
+ }
166
+
167
+ .no-records {
168
+ color: rgba(0,0,0,0.8);
169
+ font-style: italic;
170
+ font-size: 80%;
171
+ }
172
+
173
+ .details {
174
+ margin: 0 0 20px;
175
+
176
+ li {
177
+ list-style: disc;
178
+ margin: 0 0 6px 20px;
179
+
180
+ &:last-child { margin-bottom: 0; }
181
+ }
182
+ }
183
+
184
+ .back {
185
+ border-top: 1px solid #ddd;
186
+ padding: 15px 0 0;
187
+ margin: 30px 0 0;
188
+ font-size: 85%;
189
+ }
190
+ }