├── .gitignore ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── README.md ├── Rakefile ├── examples └── newsfeed │ ├── .gitignore │ ├── Gemfile │ ├── Gemfile.lock │ ├── README.rdoc │ ├── Rakefile │ ├── app │ ├── assets │ │ ├── images │ │ │ └── rails.png │ │ ├── javascripts │ │ │ ├── application.js │ │ │ ├── posts.js.coffee │ │ │ ├── reaction_image.js.coffee │ │ │ └── users.js.coffee │ │ └── stylesheets │ │ │ ├── application.css │ │ │ ├── posts.css.scss │ │ │ ├── reaction_image.css.scss │ │ │ └── users.css.scss │ ├── controllers │ │ ├── application_controller.rb │ │ ├── concerns │ │ │ └── .keep │ │ ├── posts_controller.rb │ │ └── users_controller.rb │ ├── helpers │ │ ├── application_helper.rb │ │ ├── posts_helper.rb │ │ ├── reaction_image_helper.rb │ │ └── users_helper.rb │ ├── mailers │ │ └── .keep │ ├── models │ │ ├── .keep │ │ ├── api │ │ │ ├── post.rb │ │ │ └── user.rb │ │ ├── concerns │ │ │ └── .keep │ │ ├── post.rb │ │ ├── tag.rb │ │ └── user.rb │ └── views │ │ └── layouts │ │ └── application.html.erb │ ├── aws-cli │ ├── bin │ ├── bundle │ ├── rails │ └── rake │ ├── config.ru │ ├── config │ ├── application.rb │ ├── boot.rb │ ├── database.yml │ ├── environment.rb │ ├── environments │ │ ├── development.rb │ │ ├── production.rb │ │ └── test.rb │ ├── initializers │ │ ├── backtrace_silencers.rb │ │ ├── filter_parameter_logging.rb │ │ ├── inflections.rb │ │ ├── mime_types.rb │ │ ├── secret_token.rb │ │ ├── session_store.rb │ │ └── wrap_parameters.rb │ ├── locales │ │ └── en.yml │ └── routes.rb │ ├── db │ ├── migrate │ │ ├── 20130418220504_create_posts.rb │ │ ├── 20130427094036_create_users.rb │ │ └── 20130428080634_create_tags.rb │ ├── schema.rb │ └── seeds.rb │ ├── lib │ ├── assets │ │ └── .keep │ └── tasks │ │ └── .keep │ ├── public │ ├── 404.html │ ├── 422.html │ ├── 500.html │ ├── favicon.ico │ └── robots.txt │ ├── service.json │ ├── test │ ├── controllers │ │ ├── .keep │ │ ├── posts_controller_test.rb │ │ ├── reaction_image_controller_test.rb │ │ └── users_controller_test.rb │ ├── fixtures │ │ ├── .keep │ │ ├── posts.yml │ │ ├── tags.yml │ │ └── users.yml │ ├── helpers │ │ ├── .keep │ │ ├── posts_helper_test.rb │ │ ├── reaction_image_helper_test.rb │ │ └── users_helper_test.rb │ ├── integration │ │ └── .keep │ ├── mailers │ │ └── .keep │ ├── models │ │ ├── .keep │ │ ├── post_test.rb │ │ ├── seahorse_model_test.rb │ │ ├── tag_test.rb │ │ └── user_test.rb │ └── test_helper.rb │ └── vendor │ └── assets │ ├── javascripts │ └── .keep │ └── stylesheets │ └── .keep ├── lib ├── seahorse.rb ├── seahorse │ ├── api_translator │ │ ├── inflector.rb │ │ ├── operation.rb │ │ └── shape.rb │ ├── controller.rb │ ├── model.rb │ ├── operation.rb │ ├── param_validator.rb │ ├── railtie.rb │ ├── router.rb │ ├── shape_builder.rb │ ├── type.rb │ └── version.rb └── tasks │ └── seahorse_tasks.rake └── seahorse.gemspec /.gitignore: -------------------------------------------------------------------------------- 1 | .bundle/ 2 | log/*.log 3 | pkg/ 4 | test/dummy/db/*.sqlite3 5 | test/dummy/db/*.sqlite3-journal 6 | test/dummy/log/*.log 7 | test/dummy/tmp/ 8 | test/dummy/.sass-cache 9 | examples/newsfeed/log 10 | 11 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec 4 | gem 'aws-sdk-core', path: '/Users/lsegal/aws/ruby/sdk2' 5 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | seahorse-rails-demo (0.0.1) 5 | activesupport 6 | oj 7 | 8 | PATH 9 | remote: /Users/lsegal/aws/ruby/sdk2 10 | specs: 11 | aws-sdk-core (2.0) 12 | json (~> 1.4) 13 | nokogiri (>= 1.4.6) 14 | uuidtools (~> 2.1) 15 | 16 | GEM 17 | remote: https://rubygems.org/ 18 | specs: 19 | activesupport (3.2.13) 20 | i18n (= 0.6.1) 21 | multi_json (~> 1.0) 22 | i18n (0.6.1) 23 | json (1.7.7) 24 | multi_json (1.7.2) 25 | nokogiri (1.5.9) 26 | oj (2.0.11) 27 | uuidtools (2.1.4) 28 | 29 | PLATFORMS 30 | ruby 31 | 32 | DEPENDENCIES 33 | aws-sdk-core! 34 | seahorse-rails-demo! 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2013 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"). You 4 | may not use this file except in compliance with the License. A copy of 5 | the License is located at 6 | 7 | http://aws.amazon.com/apache2.0/ 8 | 9 | or in the "license" file accompanying this file. This file is 10 | distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 11 | ANY KIND, either express or implied. See the License for the specific 12 | language governing permissions and limitations under the License. 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Seahorse 2 | 3 | Seahorse is a way to describe your service APIs as first-class citizens with 4 | a declarative DSL. The library also provides Ruby on Rails integration so that 5 | take advantage of your API model in controller actions. 6 | 7 | # Features 8 | 9 | Seahorse provides the ability to define an API model, but also has functionality 10 | to support parameter validation and serialization of inputs and outputs to API 11 | calls. With Rails integration, this is automatic, namely, parameters in 12 | `params` are automatically type-converted and validated, outputs are 13 | automatically serialized from your API model to JSON or XML. You can also hook 14 | this up to a Sinatra app with a little amount of work. 15 | 16 | # Installing 17 | 18 | ```sh 19 | gem install seahorse 20 | ``` 21 | 22 | # Usage 23 | 24 | Using Seahorse in a Rails app is pretty easy! 25 | 26 | First, define your API model and operations by creating a class like 27 | `Api::Post` in `app/models/api/post.rb` and including `Seahorse::Model`: 28 | 29 | ```ruby 30 | class Api::Post 31 | include Seahorse::Model 32 | 33 | # Define a type so you can re-use this later 34 | type :post do 35 | model ::Post # Hook up to an AR model 36 | integer :post_id, as: :id 37 | string :username, as: [:user, :username] 38 | string :body 39 | timestamp :created_at 40 | end 41 | 42 | # The 'index' action in Rails. 43 | # Also maps to the 'list_posts' command as an RPC call 44 | operation :index do 45 | # Define this if you need to map parameters in the URL 46 | url '/:username/posts' 47 | 48 | # Define some input parameters 49 | input do 50 | string :username, uri: true, required: true 51 | integer :page 52 | end 53 | 54 | # Define the output parameters 55 | output do 56 | # A list of posts 57 | list(:posts) { post } 58 | 59 | # The page number of the next page 60 | integer :next_page 61 | end 62 | end 63 | 64 | # Other operations here... 65 | end 66 | ``` 67 | 68 | You can generate a JSON description of this API model by calling: 69 | 70 | ```ruby 71 | puts Api::Post.to_json 72 | ``` 73 | 74 | ## Rails Integration 75 | 76 | In Rails, you can hook this API model up to routing by adding the following line 77 | in your `config/routes.rb`: 78 | 79 | ```ruby 80 | Seahorse::Model.add_all_routes(self) 81 | ``` 82 | 83 | This finds all API models defined and hooks them up to your Rails app. 84 | **Note** that in Rails, the Post API model will route to your `PostsController`. 85 | 86 | Then you just write Rails code as normal. Here is what the index controller 87 | action might look like on `PostsController`: 88 | 89 | ```ruby 90 | class PostsController < ApplicationController 91 | # You need to add this for Seahorse integration 92 | include Seahorse::Controller 93 | 94 | def index 95 | # Simple pagination logic 96 | page_size, page = 20, params[:page] || 1 97 | offset = (page - 1) * page_size 98 | 99 | # Build the response 100 | output = { posts: Post.limit(page_size).offset(offset) } 101 | output[:next_page] = page + 1 if Post.count > (offset + page_size) 102 | 103 | respond_with(output) 104 | end 105 | end 106 | ``` 107 | 108 | Note that `params[:page]` can be used without calling `.to_i` because Seahorse 109 | already typecasted it to an integer, since your model's input defined it as one. 110 | You no longer have to worry about typecasting values in and out. You simply 111 | take input params and call `respond_with` on the data you want to serialize 112 | out the data you want to display (also defined in your API model). 113 | 114 | Here's what a create action might look like: 115 | 116 | ```ruby 117 | def create 118 | user = User.where(username: params[:username]).first 119 | respond_with user.posts.create(params[:post]) 120 | end 121 | ``` 122 | 123 | Note that all your parameters are automatically validated and type-converted 124 | according to the inputs you defined in your API model. You don't have to white 125 | list attributes in your model, and you don't need to define strong parameters 126 | either; Seahorse does this all for you. 127 | 128 | # Contributing 129 | 130 | Feel free to open issues or submit pull requests with any ideas you can think 131 | of to make integrating the Seahorse model into your application an easier 132 | process. This project was initially created as a tech demo for RailsConf 2013 133 | to illustrate some of the principles used to design the client-side SDKs at 134 | Amazon Web Services, so the current breadth of features is fairly minimal. 135 | Feature additions and extra work on the project is welcome! 136 | 137 | # License 138 | 139 | Seahorse uses the Apache 2.0 License. See LICENSE for details. 140 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | begin 2 | require 'bundler/setup' 3 | rescue LoadError 4 | puts 'You must `gem install bundler` and `bundle install` to run rake tasks' 5 | end 6 | 7 | Bundler::GemHelper.install_tasks 8 | -------------------------------------------------------------------------------- /examples/newsfeed/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | # 3 | # If you find yourself ignoring temporary files generated by your text editor 4 | # or operating system, you probably want to add a global ignore instead: 5 | # git config --global core.excludesfile '~/.gitignore_global' 6 | 7 | # Ignore bundler config. 8 | /.bundle 9 | 10 | # Ignore the default SQLite database. 11 | /db/*.sqlite3 12 | /db/*.sqlite3-journal 13 | 14 | # Ignore all logfiles and tempfiles. 15 | /log/*.log 16 | /tmp 17 | 18 | # Seahorse 19 | vendor/seahorse -------------------------------------------------------------------------------- /examples/newsfeed/Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Bundle edge Rails instead: gem 'rails', github: 'rails/rails' 4 | gem 'rails', '~> 4.0.0.rc1' 5 | 6 | gem 'sqlite3' 7 | 8 | gem 'seahorse', :path => '../../' 9 | 10 | # Gems used only for assets and not required 11 | # in production environments by default. 12 | group :assets do 13 | gem 'sass-rails', '~> 4.0.0.rc1' 14 | gem 'coffee-rails', '~> 4.0.0.rc1' 15 | 16 | # See https://github.com/sstephenson/execjs#readme for more supported runtimes 17 | # gem 'therubyracer', platforms: :ruby 18 | 19 | gem 'uglifier', '>= 1.0.3' 20 | end 21 | 22 | gem 'jquery-rails' 23 | 24 | # Turbolinks makes following links in your web application faster. Read more: https://github.com/rails/turbolinks 25 | gem 'turbolinks' 26 | 27 | # Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder 28 | gem 'jbuilder', '~> 1.0.1' 29 | 30 | # To use ActiveModel has_secure_password 31 | # gem 'bcrypt-ruby', '~> 3.0.0' 32 | 33 | # Use unicorn as the app server 34 | # gem 'unicorn' 35 | 36 | # Deploy with Capistrano 37 | # gem 'capistrano', group: :development 38 | 39 | # To use debugger 40 | # gem 'debugger' 41 | -------------------------------------------------------------------------------- /examples/newsfeed/Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: ../../ 3 | specs: 4 | seahorse (0.1.0) 5 | activesupport 6 | oj 7 | 8 | GEM 9 | remote: https://rubygems.org/ 10 | specs: 11 | actionmailer (4.0.0.rc1) 12 | actionpack (= 4.0.0.rc1) 13 | mail (~> 2.5.3) 14 | actionpack (4.0.0.rc1) 15 | activesupport (= 4.0.0.rc1) 16 | builder (~> 3.1.0) 17 | erubis (~> 2.7.0) 18 | rack (~> 1.5.2) 19 | rack-test (~> 0.6.2) 20 | activemodel (4.0.0.rc1) 21 | activesupport (= 4.0.0.rc1) 22 | builder (~> 3.1.0) 23 | activerecord (4.0.0.rc1) 24 | activemodel (= 4.0.0.rc1) 25 | activerecord-deprecated_finders (~> 1.0.2) 26 | activesupport (= 4.0.0.rc1) 27 | arel (~> 4.0.0) 28 | activerecord-deprecated_finders (1.0.2) 29 | activesupport (4.0.0.rc1) 30 | i18n (~> 0.6, >= 0.6.4) 31 | minitest (~> 4.2) 32 | multi_json (~> 1.3) 33 | thread_safe (~> 0.1) 34 | tzinfo (~> 0.3.37) 35 | arel (4.0.0) 36 | atomic (1.1.8) 37 | builder (3.1.4) 38 | coffee-rails (4.0.0) 39 | coffee-script (>= 2.2.0) 40 | railties (>= 4.0.0.beta, < 5.0) 41 | coffee-script (2.2.0) 42 | coffee-script-source 43 | execjs 44 | coffee-script-source (1.6.2) 45 | erubis (2.7.0) 46 | execjs (1.4.0) 47 | multi_json (~> 1.0) 48 | hike (1.2.2) 49 | i18n (0.6.4) 50 | jbuilder (1.0.2) 51 | activesupport (>= 3.0.0) 52 | jquery-rails (2.2.1) 53 | railties (>= 3.0, < 5.0) 54 | thor (>= 0.14, < 2.0) 55 | mail (2.5.3) 56 | i18n (>= 0.4.0) 57 | mime-types (~> 1.16) 58 | treetop (~> 1.4.8) 59 | mime-types (1.23) 60 | minitest (4.7.3) 61 | multi_json (1.7.2) 62 | oj (2.0.12) 63 | polyglot (0.3.3) 64 | rack (1.5.2) 65 | rack-test (0.6.2) 66 | rack (>= 1.0) 67 | rails (4.0.0.rc1) 68 | actionmailer (= 4.0.0.rc1) 69 | actionpack (= 4.0.0.rc1) 70 | activerecord (= 4.0.0.rc1) 71 | activesupport (= 4.0.0.rc1) 72 | bundler (>= 1.3.0, < 2.0) 73 | railties (= 4.0.0.rc1) 74 | sprockets-rails (~> 2.0.0.rc4) 75 | railties (4.0.0.rc1) 76 | actionpack (= 4.0.0.rc1) 77 | activesupport (= 4.0.0.rc1) 78 | rake (>= 0.8.7) 79 | thor (>= 0.18.1, < 2.0) 80 | rake (10.0.4) 81 | sass (3.2.8) 82 | sass-rails (4.0.0.rc1) 83 | railties (>= 4.0.0.beta, < 5.0) 84 | sass (>= 3.1.10) 85 | sprockets-rails (~> 2.0.0.rc0) 86 | tilt (~> 1.3) 87 | sprockets (2.9.3) 88 | hike (~> 1.2) 89 | multi_json (~> 1.0) 90 | rack (~> 1.0) 91 | tilt (~> 1.1, != 1.3.0) 92 | sprockets-rails (2.0.0.rc4) 93 | actionpack (>= 3.0) 94 | activesupport (>= 3.0) 95 | sprockets (~> 2.8) 96 | sqlite3 (1.3.7) 97 | thor (0.18.1) 98 | thread_safe (0.1.0) 99 | atomic 100 | tilt (1.3.7) 101 | treetop (1.4.12) 102 | polyglot 103 | polyglot (>= 0.3.1) 104 | turbolinks (1.1.1) 105 | coffee-rails 106 | tzinfo (0.3.37) 107 | uglifier (2.0.1) 108 | execjs (>= 0.3.0) 109 | multi_json (~> 1.0, >= 1.0.2) 110 | 111 | PLATFORMS 112 | ruby 113 | 114 | DEPENDENCIES 115 | coffee-rails (~> 4.0.0.rc1) 116 | jbuilder (~> 1.0.1) 117 | jquery-rails 118 | rails (~> 4.0.0.rc1) 119 | sass-rails (~> 4.0.0.rc1) 120 | seahorse! 121 | sqlite3 122 | turbolinks 123 | uglifier (>= 1.0.3) 124 | -------------------------------------------------------------------------------- /examples/newsfeed/README.rdoc: -------------------------------------------------------------------------------- 1 | == README 2 | 3 | This README would normally document whatever steps are necessary to get the 4 | application up and running. 5 | 6 | Things you may want to cover: 7 | 8 | * Ruby version 9 | 10 | * System dependencies 11 | 12 | * Configuration 13 | 14 | * Database creation 15 | 16 | * Database initialization 17 | 18 | * How to run the test suite 19 | 20 | * Services (job queues, cache servers, search engines, etc.) 21 | 22 | * Deployment instructions 23 | 24 | * ... 25 | 26 | 27 | Please feel free to use a different markup language if you do not plan to run 28 | rake doc:app. 29 | -------------------------------------------------------------------------------- /examples/newsfeed/Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require File.expand_path('../config/application', __FILE__) 5 | 6 | NewsFeeder::Application.load_tasks 7 | -------------------------------------------------------------------------------- /examples/newsfeed/app/assets/images/rails.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amazon-archives/railsconf2013-tech-demo/279117d909c43dea888c14168a3ddd485e1eb204/examples/newsfeed/app/assets/images/rails.png -------------------------------------------------------------------------------- /examples/newsfeed/app/assets/javascripts/application.js: -------------------------------------------------------------------------------- 1 | // This is a manifest file that'll be compiled into application.js, which will include all the files 2 | // listed below. 3 | // 4 | // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, 5 | // or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path. 6 | // 7 | // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the 8 | // compiled file. 9 | // 10 | // WARNING: THE FIRST BLANK LINE MARKS THE END OF WHAT'S TO BE PROCESSED, ANY BLANK LINE SHOULD 11 | // GO AFTER THE REQUIRES BELOW. 12 | // 13 | //= require jquery 14 | //= require jquery_ujs 15 | //= require turbolinks 16 | //= require_tree . 17 | -------------------------------------------------------------------------------- /examples/newsfeed/app/assets/javascripts/posts.js.coffee: -------------------------------------------------------------------------------- 1 | # Place all the behaviors and hooks related to the matching controller here. 2 | # All this logic will automatically be available in application.js. 3 | # You can use CoffeeScript in this file: http://coffeescript.org/ 4 | -------------------------------------------------------------------------------- /examples/newsfeed/app/assets/javascripts/reaction_image.js.coffee: -------------------------------------------------------------------------------- 1 | # Place all the behaviors and hooks related to the matching controller here. 2 | # All this logic will automatically be available in application.js. 3 | # You can use CoffeeScript in this file: http://coffeescript.org/ 4 | -------------------------------------------------------------------------------- /examples/newsfeed/app/assets/javascripts/users.js.coffee: -------------------------------------------------------------------------------- 1 | # Place all the behaviors and hooks related to the matching controller here. 2 | # All this logic will automatically be available in application.js. 3 | # You can use CoffeeScript in this file: http://coffeescript.org/ 4 | -------------------------------------------------------------------------------- /examples/newsfeed/app/assets/stylesheets/application.css: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a manifest file that'll be compiled into application.css, which will include all the files 3 | * listed below. 4 | * 5 | * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, 6 | * or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path. 7 | * 8 | * You're free to add application-wide styles to this file and they'll appear at the top of the 9 | * compiled file, but it's generally better to create a new file per style scope. 10 | * 11 | *= require_self 12 | *= require_tree . 13 | */ 14 | -------------------------------------------------------------------------------- /examples/newsfeed/app/assets/stylesheets/posts.css.scss: -------------------------------------------------------------------------------- 1 | // Place all the styles related to the posts controller here. 2 | // They will automatically be included in application.css. 3 | // You can use Sass (SCSS) here: http://sass-lang.com/ 4 | -------------------------------------------------------------------------------- /examples/newsfeed/app/assets/stylesheets/reaction_image.css.scss: -------------------------------------------------------------------------------- 1 | // Place all the styles related to the reaction_image controller here. 2 | // They will automatically be included in application.css. 3 | // You can use Sass (SCSS) here: http://sass-lang.com/ 4 | -------------------------------------------------------------------------------- /examples/newsfeed/app/assets/stylesheets/users.css.scss: -------------------------------------------------------------------------------- 1 | // Place all the styles related to the users controller here. 2 | // They will automatically be included in application.css. 3 | // You can use Sass (SCSS) here: http://sass-lang.com/ 4 | -------------------------------------------------------------------------------- /examples/newsfeed/app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | # Prevent CSRF attacks by raising an exception. 3 | # For APIs, you may want to use :null_session instead. 4 | #protect_from_forgery with: :exception 5 | end 6 | -------------------------------------------------------------------------------- /examples/newsfeed/app/controllers/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amazon-archives/railsconf2013-tech-demo/279117d909c43dea888c14168a3ddd485e1eb204/examples/newsfeed/app/controllers/concerns/.keep -------------------------------------------------------------------------------- /examples/newsfeed/app/controllers/posts_controller.rb: -------------------------------------------------------------------------------- 1 | class PostsController < ApplicationController 2 | include Seahorse::Controller 3 | 4 | def index 5 | page_size, page = 20, params[:page] || 1 6 | offset = (page - 1) * page_size 7 | posts = Post.includes(:posts).limit(page_size).offset(offset) 8 | output = { posts: posts } 9 | output[:next_page] = page + 1 if Post.count > (offset + page_size) 10 | respond_with(output) 11 | end 12 | 13 | def show 14 | user = User.where(username: params[:username]).first 15 | post = user.posts.find(params[:post_id]) 16 | respond_with post 17 | end 18 | 19 | def create 20 | user = User.where(username: params[:username]).first 21 | respond_with user.posts.create(params[:post]) 22 | end 23 | 24 | def destroy 25 | Post.find(params[:post_id]).destroy 26 | respond_with success: true, deleted_at: Time.now 27 | end 28 | 29 | def repost 30 | repost_username = User.where(username: params[:repost_username]).first 31 | post = Post.find(params[:post_id]) 32 | body = params[:body] || "Repost: #{post.body}" 33 | respond_with post.posts.create(user_id: repost_username.id, body: body) 34 | end 35 | 36 | def tag 37 | user = User.where(username: params[:username]).first 38 | post = user.posts.find(params[:post_id]) 39 | post.update_attributes(params[:post]) 40 | respond_with post 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /examples/newsfeed/app/controllers/users_controller.rb: -------------------------------------------------------------------------------- 1 | class UsersController < ApplicationController 2 | include Seahorse::Controller 3 | 4 | def index 5 | respond_with User.all 6 | end 7 | 8 | def show 9 | respond_with User.where(params).first 10 | end 11 | 12 | def follow 13 | user1 = User.where(username: params[:username]).first 14 | user2 = User.where(username: params[:following_username]).first 15 | user1.following << user2 16 | respond_with success: true, followed_at: Time.now 17 | end 18 | 19 | def create 20 | respond_with User.create(params) 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /examples/newsfeed/app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | end 3 | -------------------------------------------------------------------------------- /examples/newsfeed/app/helpers/posts_helper.rb: -------------------------------------------------------------------------------- 1 | module PostsHelper 2 | end 3 | -------------------------------------------------------------------------------- /examples/newsfeed/app/helpers/reaction_image_helper.rb: -------------------------------------------------------------------------------- 1 | module ReactionImageHelper 2 | end 3 | -------------------------------------------------------------------------------- /examples/newsfeed/app/helpers/users_helper.rb: -------------------------------------------------------------------------------- 1 | module UsersHelper 2 | end 3 | -------------------------------------------------------------------------------- /examples/newsfeed/app/mailers/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amazon-archives/railsconf2013-tech-demo/279117d909c43dea888c14168a3ddd485e1eb204/examples/newsfeed/app/mailers/.keep -------------------------------------------------------------------------------- /examples/newsfeed/app/models/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amazon-archives/railsconf2013-tech-demo/279117d909c43dea888c14168a3ddd485e1eb204/examples/newsfeed/app/models/.keep -------------------------------------------------------------------------------- /examples/newsfeed/app/models/api/post.rb: -------------------------------------------------------------------------------- 1 | class Api::Post 2 | include Seahorse::Model 3 | 4 | type :post => :structure do 5 | model ::Post 6 | integer :post_id, as: :id 7 | string :username, as: [:user, :username] 8 | string :body 9 | list(:tags) { tag } 10 | integer :repost_count, as: [:posts, :count] 11 | timestamp :created_at 12 | end 13 | 14 | type :post_details => :post do 15 | list :reposts, as: :posts do 16 | structure do 17 | integer :post_id, as: :id 18 | string :username, as: [:user, :username] 19 | string :body 20 | end 21 | end 22 | end 23 | 24 | type :tag => :structure do 25 | model Tag 26 | string :name 27 | end 28 | 29 | type :post_id => :structure do 30 | string :username, uri: true, required: true 31 | integer :post_id, uri: true, required: true, as: :id 32 | end 33 | 34 | desc "Creates a new post" 35 | operation :create do 36 | url '/:username/posts' 37 | 38 | input do 39 | desc "The username to create a post for" 40 | string :username, uri: true, required: true 41 | 42 | structure :post do 43 | desc "The body contents of the post" 44 | string :body, required: true 45 | 46 | list(:tags) { tag } 47 | end 48 | end 49 | 50 | output :post 51 | end 52 | 53 | operation :repost do 54 | url '/:username/posts/:post_id/repost/:repost_username' 55 | verb :post 56 | 57 | input do 58 | string :username, uri: true, required: true 59 | string :repost_username, uri: true, required: true 60 | integer :post_id, uri: true, required: true, as: :id 61 | string :body 62 | end 63 | 64 | output :post 65 | end 66 | 67 | operation :tag do 68 | url '/:username/posts/:post_id/tag' 69 | 70 | input :post_id do 71 | structure :post do 72 | list(:tags) { tag } 73 | end 74 | end 75 | 76 | output :post 77 | end 78 | 79 | operation :destroy do 80 | url '/:username/posts/:post_id' 81 | 82 | input :post_id 83 | 84 | output do 85 | boolean :success 86 | timestamp :deleted_at 87 | end 88 | end 89 | 90 | operation :index do 91 | url '/:username/posts' 92 | 93 | input do 94 | string :username, uri: true, required: true 95 | integer :page 96 | end 97 | 98 | output do 99 | list(:posts) { post } 100 | integer :next_page 101 | end 102 | end 103 | 104 | operation :show do 105 | url '/:username/posts/:post_id' 106 | 107 | input :post_id 108 | output :post_details 109 | end 110 | end 111 | -------------------------------------------------------------------------------- /examples/newsfeed/app/models/api/user.rb: -------------------------------------------------------------------------------- 1 | class Api::User 2 | include Seahorse::Model 3 | 4 | type :username => :string 5 | 6 | type :user do 7 | model ::User 8 | username 9 | list(:followers) { username as: :username } 10 | integer :followers_count, as: [:followers, :count] 11 | list(:following) { username as: :username } 12 | integer :following_count, as: [:following, :count] 13 | timestamp :created_at 14 | end 15 | 16 | operation :create do 17 | url '/:username' 18 | 19 | input do 20 | string :username, uri: true, required: true 21 | end 22 | 23 | output :user 24 | end 25 | 26 | operation :follow do 27 | url '/:username/follow/:following_username' 28 | verb :post 29 | 30 | input do 31 | string :username, uri: true, required: true 32 | string :following_username, uri: true, required: true 33 | end 34 | 35 | output do 36 | boolean :success 37 | timestamp :followed_at 38 | end 39 | end 40 | 41 | operation :index do 42 | output(:list) { username as: [:user, :username] } 43 | end 44 | 45 | operation :show do 46 | url '/:username' 47 | input { string :username, uri: true, required: true } 48 | output :user 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /examples/newsfeed/app/models/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amazon-archives/railsconf2013-tech-demo/279117d909c43dea888c14168a3ddd485e1eb204/examples/newsfeed/app/models/concerns/.keep -------------------------------------------------------------------------------- /examples/newsfeed/app/models/post.rb: -------------------------------------------------------------------------------- 1 | class Post < ActiveRecord::Base 2 | belongs_to :user 3 | has_many :posts 4 | has_many :tags 5 | accepts_nested_attributes_for :tags, allow_destroy: true 6 | end 7 | -------------------------------------------------------------------------------- /examples/newsfeed/app/models/tag.rb: -------------------------------------------------------------------------------- 1 | class Tag < ActiveRecord::Base 2 | belongs_to :post 3 | end 4 | -------------------------------------------------------------------------------- /examples/newsfeed/app/models/user.rb: -------------------------------------------------------------------------------- 1 | class User < ActiveRecord::Base 2 | has_many :posts 3 | has_and_belongs_to_many :followers, class_name: "User", 4 | foreign_key: :subscriber_id, association_foreign_key: :user_id 5 | has_and_belongs_to_many :following, class_name: "User", 6 | association_foreign_key: :subscriber_id 7 | end 8 | -------------------------------------------------------------------------------- /examples/newsfeed/app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |You may have mistyped the address or the page may have moved.
24 |If you are the application owner check the logs for more information.
26 | 27 | 28 | -------------------------------------------------------------------------------- /examples/newsfeed/public/422.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |Maybe you tried to change something you didn't have access to.
24 |If you are the application owner check the logs for more information.
25 | 26 | 27 | -------------------------------------------------------------------------------- /examples/newsfeed/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amazon-archives/railsconf2013-tech-demo/279117d909c43dea888c14168a3ddd485e1eb204/examples/newsfeed/public/favicon.ico -------------------------------------------------------------------------------- /examples/newsfeed/public/robots.txt: -------------------------------------------------------------------------------- 1 | # See http://www.robotstxt.org/wc/norobots.html for documentation on how to use the robots.txt file 2 | # 3 | # To ban all spiders from the entire site uncomment the next two lines: 4 | # User-agent: * 5 | # Disallow: / 6 | -------------------------------------------------------------------------------- /examples/newsfeed/service.json: -------------------------------------------------------------------------------- 1 | { 2 | "format":"rest-json", 3 | "type":"rest-json", 4 | "endpoint_prefix":"", 5 | "operations":{ 6 | "createPost":{ 7 | "name":"CreatePost", 8 | "http":{ 9 | "uri":"/{username}/posts", 10 | "method":"POST" 11 | }, 12 | "input":{ 13 | "type":"structure", 14 | "members":{ 15 | "username":{ 16 | "type":"string", 17 | "required":true, 18 | "location":"uri", 19 | "documentation":"The username to create a post for" 20 | }, 21 | "post":{ 22 | "type":"structure", 23 | "members":{ 24 | "body":{ 25 | "type":"string", 26 | "required":true, 27 | "documentation":"The body contents of the post" 28 | }, 29 | "tags":{ 30 | "type":"list", 31 | "members":{ 32 | "type":"structure", 33 | "members":{ 34 | "name":{ 35 | "type":"string" 36 | } 37 | } 38 | } 39 | } 40 | } 41 | } 42 | } 43 | }, 44 | "output":{ 45 | "type":"structure", 46 | "members":{ 47 | "post_id":{ 48 | "type":"integer" 49 | }, 50 | "username":{ 51 | "type":"string" 52 | }, 53 | "body":{ 54 | "type":"string" 55 | }, 56 | "tags":{ 57 | "type":"list", 58 | "members":{ 59 | "type":"structure", 60 | "members":{ 61 | "name":{ 62 | "type":"string" 63 | } 64 | } 65 | } 66 | }, 67 | "repost_count":{ 68 | "type":"integer" 69 | }, 70 | "created_at":{ 71 | "type":"timestamp" 72 | } 73 | } 74 | }, 75 | "documentation":"Creates a new post" 76 | }, 77 | "repostPost":{ 78 | "name":"RepostPost", 79 | "http":{ 80 | "uri":"/{username}/posts/{post_id}/repost/{repost_username}", 81 | "method":":POST" 82 | }, 83 | "input":{ 84 | "type":"structure", 85 | "members":{ 86 | "username":{ 87 | "type":"string", 88 | "required":true, 89 | "location":"uri" 90 | }, 91 | "repost_username":{ 92 | "type":"string", 93 | "required":true, 94 | "location":"uri" 95 | }, 96 | "post_id":{ 97 | "type":"integer", 98 | "required":true, 99 | "location":"uri" 100 | }, 101 | "body":{ 102 | "type":"string" 103 | } 104 | } 105 | }, 106 | "output":{ 107 | "type":"structure", 108 | "members":{ 109 | "post_id":{ 110 | "type":"integer" 111 | }, 112 | "username":{ 113 | "type":"string" 114 | }, 115 | "body":{ 116 | "type":"string" 117 | }, 118 | "tags":{ 119 | "type":"list", 120 | "members":{ 121 | "type":"structure", 122 | "members":{ 123 | "name":{ 124 | "type":"string" 125 | } 126 | } 127 | } 128 | }, 129 | "repost_count":{ 130 | "type":"integer" 131 | }, 132 | "created_at":{ 133 | "type":"timestamp" 134 | } 135 | } 136 | }, 137 | "documentation":null 138 | }, 139 | "tagPost":{ 140 | "name":"TagPost", 141 | "http":{ 142 | "uri":"/{username}/posts/{post_id}/tag", 143 | "method":"GET" 144 | }, 145 | "input":{ 146 | "type":"structure", 147 | "members":{ 148 | "username":{ 149 | "type":"string", 150 | "required":true, 151 | "location":"uri" 152 | }, 153 | "post_id":{ 154 | "type":"integer", 155 | "required":true, 156 | "location":"uri" 157 | }, 158 | "post":{ 159 | "type":"structure", 160 | "members":{ 161 | "tags":{ 162 | "type":"list", 163 | "members":{ 164 | "type":"structure", 165 | "members":{ 166 | "name":{ 167 | "type":"string" 168 | } 169 | } 170 | } 171 | } 172 | } 173 | } 174 | } 175 | }, 176 | "output":{ 177 | "type":"structure", 178 | "members":{ 179 | "post_id":{ 180 | "type":"integer" 181 | }, 182 | "username":{ 183 | "type":"string" 184 | }, 185 | "body":{ 186 | "type":"string" 187 | }, 188 | "tags":{ 189 | "type":"list", 190 | "members":{ 191 | "type":"structure", 192 | "members":{ 193 | "name":{ 194 | "type":"string" 195 | } 196 | } 197 | } 198 | }, 199 | "repost_count":{ 200 | "type":"integer" 201 | }, 202 | "created_at":{ 203 | "type":"timestamp" 204 | } 205 | } 206 | }, 207 | "documentation":null 208 | }, 209 | "destroyPost":{ 210 | "name":"DestroyPost", 211 | "http":{ 212 | "uri":"/{username}/posts/{post_id}", 213 | "method":"DELETE" 214 | }, 215 | "input":{ 216 | "type":"structure", 217 | "members":{ 218 | "username":{ 219 | "type":"string", 220 | "required":true, 221 | "location":"uri" 222 | }, 223 | "post_id":{ 224 | "type":"integer", 225 | "required":true, 226 | "location":"uri" 227 | } 228 | } 229 | }, 230 | "output":{ 231 | "type":"structure", 232 | "members":{ 233 | "success":{ 234 | "type":"boolean" 235 | }, 236 | "deleted_at":{ 237 | "type":"timestamp" 238 | } 239 | } 240 | }, 241 | "documentation":null 242 | }, 243 | "listPosts":{ 244 | "name":"ListPosts", 245 | "http":{ 246 | "uri":"/{username}/posts", 247 | "method":"GET" 248 | }, 249 | "input":{ 250 | "type":"structure", 251 | "members":{ 252 | "username":{ 253 | "type":"string", 254 | "required":true, 255 | "location":"uri" 256 | }, 257 | "page":{ 258 | "type":"integer" 259 | } 260 | } 261 | }, 262 | "output":{ 263 | "type":"structure", 264 | "members":{ 265 | "posts":{ 266 | "type":"list", 267 | "members":{ 268 | "type":"structure", 269 | "members":{ 270 | "post_id":{ 271 | "type":"integer" 272 | }, 273 | "username":{ 274 | "type":"string" 275 | }, 276 | "body":{ 277 | "type":"string" 278 | }, 279 | "tags":{ 280 | "type":"list", 281 | "members":{ 282 | "type":"structure", 283 | "members":{ 284 | "name":{ 285 | "type":"string" 286 | } 287 | } 288 | } 289 | }, 290 | "repost_count":{ 291 | "type":"integer" 292 | }, 293 | "created_at":{ 294 | "type":"timestamp" 295 | } 296 | } 297 | } 298 | }, 299 | "next_page":{ 300 | "type":"integer" 301 | } 302 | } 303 | }, 304 | "documentation":null 305 | }, 306 | "getPost":{ 307 | "name":"GetPost", 308 | "http":{ 309 | "uri":"/{username}/posts/{post_id}", 310 | "method":"GET" 311 | }, 312 | "input":{ 313 | "type":"structure", 314 | "members":{ 315 | "username":{ 316 | "type":"string", 317 | "required":true, 318 | "location":"uri" 319 | }, 320 | "post_id":{ 321 | "type":"integer", 322 | "required":true, 323 | "location":"uri" 324 | } 325 | } 326 | }, 327 | "output":{ 328 | "type":"structure", 329 | "members":{ 330 | "post_id":{ 331 | "type":"integer" 332 | }, 333 | "username":{ 334 | "type":"string" 335 | }, 336 | "body":{ 337 | "type":"string" 338 | }, 339 | "tags":{ 340 | "type":"list", 341 | "members":{ 342 | "type":"structure", 343 | "members":{ 344 | "name":{ 345 | "type":"string" 346 | } 347 | } 348 | } 349 | }, 350 | "repost_count":{ 351 | "type":"integer" 352 | }, 353 | "created_at":{ 354 | "type":"timestamp" 355 | }, 356 | "reposts":{ 357 | "type":"list", 358 | "members":{ 359 | "type":"structure", 360 | "members":{ 361 | "post_id":{ 362 | "type":"integer" 363 | }, 364 | "username":{ 365 | "type":"string" 366 | }, 367 | "body":{ 368 | "type":"string" 369 | } 370 | } 371 | } 372 | } 373 | } 374 | }, 375 | "documentation":null 376 | }, 377 | "createUser":{ 378 | "name":"CreateUser", 379 | "http":{ 380 | "uri":"/{username}", 381 | "method":"POST" 382 | }, 383 | "input":{ 384 | "type":"structure", 385 | "members":{ 386 | "username":{ 387 | "type":"string", 388 | "required":true, 389 | "location":"uri" 390 | } 391 | } 392 | }, 393 | "output":{ 394 | "type":"structure", 395 | "members":{ 396 | "username":{ 397 | "type":"string" 398 | }, 399 | "followers":{ 400 | "type":"list", 401 | "members":{ 402 | "type":"string" 403 | } 404 | }, 405 | "followers_count":{ 406 | "type":"integer" 407 | }, 408 | "following":{ 409 | "type":"list", 410 | "members":{ 411 | "type":"string" 412 | } 413 | }, 414 | "following_count":{ 415 | "type":"integer" 416 | }, 417 | "created_at":{ 418 | "type":"timestamp" 419 | } 420 | } 421 | }, 422 | "documentation":null 423 | }, 424 | "followUser":{ 425 | "name":"FollowUser", 426 | "http":{ 427 | "uri":"/{username}/follow/{following_username}", 428 | "method":":POST" 429 | }, 430 | "input":{ 431 | "type":"structure", 432 | "members":{ 433 | "username":{ 434 | "type":"string", 435 | "required":true, 436 | "location":"uri" 437 | }, 438 | "following_username":{ 439 | "type":"string", 440 | "required":true, 441 | "location":"uri" 442 | } 443 | } 444 | }, 445 | "output":{ 446 | "type":"structure", 447 | "members":{ 448 | "success":{ 449 | "type":"boolean" 450 | }, 451 | "followed_at":{ 452 | "type":"timestamp" 453 | } 454 | } 455 | }, 456 | "documentation":null 457 | }, 458 | "listUsers":{ 459 | "name":"ListUsers", 460 | "http":{ 461 | "uri":"/users", 462 | "method":"GET" 463 | }, 464 | "input":{ 465 | "type":"structure", 466 | "members":{} 467 | }, 468 | "output":{ 469 | "type":"list", 470 | "members":{ 471 | "type":"string" 472 | } 473 | }, 474 | "documentation":null 475 | }, 476 | "getUser":{ 477 | "name":"GetUser", 478 | "http":{ 479 | "uri":"/{username}", 480 | "method":"GET" 481 | }, 482 | "input":{ 483 | "type":"structure", 484 | "members":{ 485 | "username":{ 486 | "type":"string", 487 | "required":true, 488 | "location":"uri" 489 | } 490 | } 491 | }, 492 | "output":{ 493 | "type":"structure", 494 | "members":{ 495 | "username":{ 496 | "type":"string" 497 | }, 498 | "followers":{ 499 | "type":"list", 500 | "members":{ 501 | "type":"string" 502 | } 503 | }, 504 | "followers_count":{ 505 | "type":"integer" 506 | }, 507 | "following":{ 508 | "type":"list", 509 | "members":{ 510 | "type":"string" 511 | } 512 | }, 513 | "following_count":{ 514 | "type":"integer" 515 | }, 516 | "created_at":{ 517 | "type":"timestamp" 518 | } 519 | } 520 | }, 521 | "documentation":null 522 | } 523 | }} 524 | -------------------------------------------------------------------------------- /examples/newsfeed/test/controllers/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amazon-archives/railsconf2013-tech-demo/279117d909c43dea888c14168a3ddd485e1eb204/examples/newsfeed/test/controllers/.keep -------------------------------------------------------------------------------- /examples/newsfeed/test/controllers/posts_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class PostsControllerTest < ActionController::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /examples/newsfeed/test/controllers/reaction_image_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class ReactionImageControllerTest < ActionController::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /examples/newsfeed/test/controllers/users_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class UsersControllerTest < ActionController::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /examples/newsfeed/test/fixtures/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amazon-archives/railsconf2013-tech-demo/279117d909c43dea888c14168a3ddd485e1eb204/examples/newsfeed/test/fixtures/.keep -------------------------------------------------------------------------------- /examples/newsfeed/test/fixtures/posts.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/Fixtures.html 2 | 3 | # This model initially had no columns defined. If you add columns to the 4 | # model remove the '{}' from the fixture names and add the columns immediately 5 | # below each fixture, per the syntax in the comments below 6 | # 7 | one: 8 | id: 1 9 | body: Body! 10 | user_id: 1 11 | 12 | two: 13 | id: 2 14 | body: "Reposted: Body!" 15 | user_id: 2 16 | post_id: 1 17 | -------------------------------------------------------------------------------- /examples/newsfeed/test/fixtures/tags.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/Fixtures.html 2 | 3 | one: 4 | name: MyString 5 | 6 | two: 7 | name: MyString 8 | -------------------------------------------------------------------------------- /examples/newsfeed/test/fixtures/users.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/Fixtures.html 2 | 3 | one: 4 | id: 1 5 | username: lsegal 6 | 7 | two: 8 | id: 2 9 | username: other_user 10 | 11 | three: 12 | id: 3 13 | username: foo 14 | -------------------------------------------------------------------------------- /examples/newsfeed/test/helpers/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amazon-archives/railsconf2013-tech-demo/279117d909c43dea888c14168a3ddd485e1eb204/examples/newsfeed/test/helpers/.keep -------------------------------------------------------------------------------- /examples/newsfeed/test/helpers/posts_helper_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class PostsHelperTest < ActionView::TestCase 4 | end 5 | -------------------------------------------------------------------------------- /examples/newsfeed/test/helpers/reaction_image_helper_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class ReactionImageHelperTest < ActionView::TestCase 4 | end 5 | -------------------------------------------------------------------------------- /examples/newsfeed/test/helpers/users_helper_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class UsersHelperTest < ActionView::TestCase 4 | end 5 | -------------------------------------------------------------------------------- /examples/newsfeed/test/integration/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amazon-archives/railsconf2013-tech-demo/279117d909c43dea888c14168a3ddd485e1eb204/examples/newsfeed/test/integration/.keep -------------------------------------------------------------------------------- /examples/newsfeed/test/mailers/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amazon-archives/railsconf2013-tech-demo/279117d909c43dea888c14168a3ddd485e1eb204/examples/newsfeed/test/mailers/.keep -------------------------------------------------------------------------------- /examples/newsfeed/test/models/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amazon-archives/railsconf2013-tech-demo/279117d909c43dea888c14168a3ddd485e1eb204/examples/newsfeed/test/models/.keep -------------------------------------------------------------------------------- /examples/newsfeed/test/models/post_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class PostTest < ActiveSupport::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /examples/newsfeed/test/models/seahorse_model_test.rb: -------------------------------------------------------------------------------- 1 | require_relative '../test_helper' 2 | 3 | class SeahorseModelTest < ActiveSupport::TestCase 4 | test "to_hash" 5 | end 6 | -------------------------------------------------------------------------------- /examples/newsfeed/test/models/tag_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class TagTest < ActiveSupport::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /examples/newsfeed/test/models/user_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class UserTest < ActiveSupport::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /examples/newsfeed/test/test_helper.rb: -------------------------------------------------------------------------------- 1 | ENV["RAILS_ENV"] = "test" 2 | require File.expand_path('../../config/environment', __FILE__) 3 | require 'rails/test_help' 4 | 5 | class ActiveSupport::TestCase 6 | ActiveRecord::Migration.check_pending! 7 | 8 | # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. 9 | # 10 | # Note: You'll currently still have to declare fixtures explicitly in integration tests 11 | # -- they do not yet inherit this setting 12 | fixtures :all 13 | 14 | # Add more helper methods to be used by all tests here... 15 | end 16 | -------------------------------------------------------------------------------- /examples/newsfeed/vendor/assets/javascripts/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amazon-archives/railsconf2013-tech-demo/279117d909c43dea888c14168a3ddd485e1eb204/examples/newsfeed/vendor/assets/javascripts/.keep -------------------------------------------------------------------------------- /examples/newsfeed/vendor/assets/stylesheets/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amazon-archives/railsconf2013-tech-demo/279117d909c43dea888c14168a3ddd485e1eb204/examples/newsfeed/vendor/assets/stylesheets/.keep -------------------------------------------------------------------------------- /lib/seahorse.rb: -------------------------------------------------------------------------------- 1 | require 'active_support/core_ext/hash/indifferent_access' 2 | require 'active_support/concern' 3 | require 'active_support/inflector' 4 | 5 | require 'seahorse/api_translator/operation' 6 | require 'seahorse/api_translator/shape' 7 | require 'seahorse/api_translator/inflector' 8 | require 'seahorse/controller' 9 | require 'seahorse/router' 10 | require 'seahorse/model' 11 | require 'seahorse/operation' 12 | require 'seahorse/type' 13 | require 'seahorse/shape_builder' 14 | require 'seahorse/version' 15 | 16 | require 'seahorse/railtie' if defined?(Rails) -------------------------------------------------------------------------------- /lib/seahorse/api_translator/inflector.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2011-2013 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"). You 4 | # may not use this file except in compliance with the License. A copy of 5 | # the License is located at 6 | # 7 | # http://aws.amazon.com/apache2.0/ 8 | # 9 | # or in the "license" file accompanying this file. This file is 10 | # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 11 | # ANY KIND, either express or implied. See the License for the specific 12 | # language governing permissions and limitations under the License. 13 | 14 | module Seahorse 15 | class ApiTranslator 16 | 17 | # @private 18 | module Inflector 19 | 20 | # Performs a very simple inflection on on the words as they are 21 | # formatted in the source API configurations. These are *not* 22 | # general case inflectors. 23 | # @param [String] string The string to inflect. 24 | # @param [String,nil] format Valid formats include 'snake_case', 25 | # 'camelCase' and `nil` (leave as is). 26 | # @return [String] 27 | def inflect string, format = nil 28 | case format 29 | when 'camelCase' then string.camelize 30 | when 'snake_case' then string.underscore 31 | else string 32 | end 33 | end 34 | end 35 | 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/seahorse/api_translator/operation.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2011-2013 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"). You 4 | # may not use this file except in compliance with the License. A copy of 5 | # the License is located at 6 | # 7 | # http://aws.amazon.com/apache2.0/ 8 | # 9 | # or in the "license" file accompanying this file. This file is 10 | # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 11 | # ANY KIND, either express or implied. See the License for the specific 12 | # language governing permissions and limitations under the License. 13 | 14 | require_relative './inflector' 15 | require_relative './shape' 16 | 17 | module Seahorse 18 | class ApiTranslator 19 | 20 | # @private 21 | class Operation 22 | 23 | include Inflector 24 | 25 | def initialize rules, options = {} 26 | @options = options 27 | 28 | @method_name = rules['name'].sub(/\d{4}_\d{2}_\d{2}$/, '') 29 | @method_name = inflect(@method_name, @options[:inflect_method_names]) 30 | 31 | @rules = rules 32 | 33 | if @rules['http'] 34 | @rules['http'].delete('response_code') 35 | end 36 | 37 | translate_input 38 | translate_output 39 | 40 | if @options[:documentation] 41 | @rules['errors'] = @rules['errors'].map {|e| e['shape_name'] } 42 | else 43 | @rules.delete('errors') 44 | @rules.delete('documentation') 45 | @rules.delete('documentation_url') 46 | @rules.delete('response_code') 47 | end 48 | end 49 | 50 | # @return [String] 51 | attr_reader :method_name 52 | 53 | # @return [Hash] 54 | attr_reader :rules 55 | 56 | private 57 | 58 | def translate_input 59 | if @rules['input'] 60 | rules = InputShape.new(@rules['input'], @options).rules 61 | rules['members'] ||= {} 62 | rules = normalize_inputs(rules) 63 | else 64 | rules = { 65 | 'type' => 'structure', 66 | 'members' => {}, 67 | } 68 | end 69 | @rules['input'] = rules 70 | end 71 | 72 | def translate_output 73 | if @rules['output'] 74 | rules = OutputShape.new(@rules['output'], @options).rules 75 | move_up_outputs(rules) 76 | cache_payload(rules) 77 | else 78 | rules = { 79 | 'type' => 'structure', 80 | 'members' => {}, 81 | } 82 | end 83 | @rules['output'] = rules 84 | end 85 | 86 | def normalize_inputs rules 87 | return rules unless @options[:type].match(/rest/) 88 | 89 | xml = @options[:type].match(/xml/) 90 | payload = false 91 | wrapper = false 92 | 93 | if rules['members'].any?{|name,rule| rule['payload'] } 94 | 95 | # exactly one member has the payload trait 96 | payload, rule = rules['members'].find{|name,rule| rule['payload'] } 97 | rule.delete('payload') 98 | 99 | #if rule['type'] == 'structure' 100 | # wrapper = payload 101 | # payload = [payload] 102 | #end 103 | 104 | else 105 | 106 | # no members marked themselves as the payload, collect everything 107 | # without a location 108 | payload = rules['members'].inject([]) do |list,(name,rule)| 109 | list << name if !rule['location'] 110 | list 111 | end 112 | 113 | if payload.empty? 114 | payload = false 115 | elsif xml 116 | wrapper = @rules['input']['shape_name'] 117 | end 118 | 119 | end 120 | 121 | rules = { 'wrapper' => wrapper }.merge(rules) if wrapper 122 | rules = { 'payload' => payload }.merge(rules) if payload 123 | rules 124 | 125 | end 126 | 127 | def move_up_outputs output 128 | move_up = nil 129 | (output['members'] || {}).each_pair do |member_name, rules| 130 | if rules['payload'] and rules['type'] == 'structure' 131 | rules.delete('payload') 132 | move_up = member_name 133 | end 134 | end 135 | 136 | if move_up 137 | output['members'].merge!(output['members'].delete(move_up)['members']) 138 | end 139 | end 140 | 141 | def cache_payload rules 142 | (rules['members'] || {}).each_pair do |member_name, rule| 143 | rules['payload'] = member_name if rule['payload'] || rule['streaming'] 144 | rule.delete('payload') 145 | end 146 | end 147 | 148 | end 149 | end 150 | end 151 | -------------------------------------------------------------------------------- /lib/seahorse/api_translator/shape.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2011-2013 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"). You 4 | # may not use this file except in compliance with the License. A copy of 5 | # the License is located at 6 | # 7 | # http://aws.amazon.com/apache2.0/ 8 | # 9 | # or in the "license" file accompanying this file. This file is 10 | # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 11 | # ANY KIND, either express or implied. See the License for the specific 12 | # language governing permissions and limitations under the License. 13 | 14 | require_relative './inflector' 15 | 16 | module Seahorse 17 | class ApiTranslator 18 | 19 | # @private 20 | class Shape 21 | 22 | include Inflector 23 | 24 | def initialize rules, options = {} 25 | @options = options 26 | @rules = {} 27 | @rules['name'] = options['name'] if options.key?('name') 28 | set_type(rules.delete('type')) 29 | rules.each_pair do |method,arg| 30 | send("set_#{method}", *[arg]) 31 | end 32 | end 33 | 34 | def rules 35 | if @rules['type'] != 'blob' 36 | @rules 37 | elsif @rules['payload'] or @rules['streaming'] 38 | @rules.merge('type' => 'binary') 39 | else 40 | @rules.merge('type' => 'base64') 41 | end 42 | end 43 | 44 | def xmlname 45 | if @rules['flattened'] 46 | (@rules['members'] || {})['name'] || @xmlname 47 | else 48 | @xmlname 49 | end 50 | end 51 | 52 | protected 53 | 54 | def set_timestamp_format format 55 | @rules['format'] = format 56 | end 57 | 58 | def set_type name 59 | types = { 60 | 'structure' => 'structure', 61 | 'list' => 'list', 62 | 'map' => 'map', 63 | 'boolean' => 'boolean', 64 | 'timestamp' => 'timestamp', 65 | 'character' => 'string', 66 | 'double' => 'float', 67 | 'float' => 'float', 68 | 'integer' => 'integer', 69 | 'long' => 'integer', 70 | 'short' => 'integer', 71 | 'string' => 'string', 72 | 'blob' => 'blob', 73 | 'biginteger' => 'integer', 74 | 'bigdecimal' => 'float', 75 | } 76 | if name == 'string' 77 | # Purposefully omitting type when string (to reduce size of the api 78 | # configuration). The parsers use string as the default when 79 | # 'type' is omitted. 80 | #@rules['type'] = 'string' 81 | elsif type = types[name] 82 | @rules['type'] = type 83 | else 84 | raise "unhandled shape type: #{name}" 85 | end 86 | end 87 | 88 | def set_members members 89 | case @rules['type'] 90 | when 'structure' 91 | @rules['members'] = {} 92 | members.each_pair do |member_name,member_rules| 93 | 94 | member_shape = new_shape(member_rules) 95 | 96 | member_key = inflect(member_name, @options[:inflect_member_names]) 97 | member_rules = member_shape.rules 98 | 99 | if member_name != member_key 100 | member_rules = { 'name' => member_name }.merge(member_rules) 101 | end 102 | 103 | if swap_names?(member_shape) 104 | member_rules['name'] = member_key 105 | member_key = member_shape.xmlname 106 | end 107 | 108 | @rules['members'][member_key] = member_rules 109 | 110 | end 111 | when 'list' 112 | @rules['members'] = new_shape(members).rules 113 | when 'map' 114 | @rules['members'] = new_shape(members).rules 115 | else 116 | raise "unhandled complex shape `#{@rules['type']}'" 117 | end 118 | @rules.delete('members') if @rules['members'].empty? 119 | end 120 | 121 | def set_keys rules 122 | shape = new_shape(rules) 123 | @rules['keys'] = shape.rules 124 | @rules.delete('keys') if @rules['keys'].empty? 125 | end 126 | 127 | def set_xmlname name 128 | @xmlname = name 129 | @rules['name'] = name 130 | end 131 | 132 | def set_location location 133 | @rules['location'] = (location == 'http_status' ? 'status' : location) 134 | end 135 | 136 | def set_location_name header_name 137 | @rules['name'] = header_name 138 | end 139 | 140 | def set_payload state 141 | @rules['payload'] = true if state 142 | end 143 | 144 | def set_flattened state 145 | @rules['flattened'] = true if state 146 | end 147 | 148 | def set_streaming state 149 | @rules['streaming'] = true if state 150 | end 151 | 152 | def set_xmlnamespace ns 153 | @rules['xmlns'] = ns 154 | end 155 | 156 | def set_xmlattribute state 157 | @rules['attribute'] = true if state 158 | end 159 | 160 | def set_documentation docs 161 | @rules['documentation'] = docs if @options[:documentation] 162 | end 163 | 164 | def set_enum values 165 | @rules['enum'] = values if @options[:documentation] 166 | end 167 | 168 | def set_wrapper state 169 | @rules['wrapper'] = true if state 170 | end 171 | 172 | # we purposefully drop these, not useful unless you want to create 173 | # static classes 174 | def set_shape_name *args; end 175 | def set_box *args; end 176 | 177 | # @param [Hash] rules 178 | # @option options [String] :name The name this shape has as a structure member. 179 | def new_shape rules 180 | self.class.new(rules, @options) 181 | end 182 | 183 | end 184 | 185 | # @private 186 | class InputShape < Shape 187 | 188 | def set_required *args 189 | @rules['required'] = true; 190 | end 191 | 192 | def set_member_order order 193 | @rules['order'] = order 194 | end 195 | 196 | def set_min_length min 197 | @rules['min_length'] = min if @options[:documentation] 198 | end 199 | 200 | def set_max_length max 201 | @rules['max_length'] = max if @options[:documentation] 202 | end 203 | 204 | def set_pattern pattern 205 | @rules['pattern'] = pattern if @options[:documentation] 206 | end 207 | 208 | def swap_names? shape 209 | false 210 | end 211 | 212 | end 213 | 214 | # @private 215 | class OutputShape < Shape 216 | 217 | # these traits are ignored for output shapes 218 | def set_required *args; end 219 | def set_member_order *args; end 220 | def set_min_length *args; end 221 | def set_max_length *args; end 222 | def set_pattern *args; end 223 | 224 | def swap_names? shape 225 | if @options[:documentation] 226 | false 227 | else 228 | !!(%w(query rest-xml).include?(@options[:type]) and shape.xmlname) 229 | end 230 | end 231 | 232 | end 233 | 234 | end 235 | end 236 | -------------------------------------------------------------------------------- /lib/seahorse/controller.rb: -------------------------------------------------------------------------------- 1 | require_relative './param_validator' 2 | 3 | module Seahorse 4 | module Controller 5 | extend ActiveSupport::Concern 6 | 7 | included do 8 | respond_to :json, :xml 9 | 10 | rescue_from Exception, :with => :render_error 11 | 12 | wrap_parameters false 13 | 14 | before_filter do 15 | @params = params 16 | @params = operation.input.from_input(params, false) 17 | @params.update(operation.input.from_input(map_headers, false)) 18 | 19 | begin 20 | input_rules = operation.to_hash['input'] 21 | %w(action controller format).each {|v| params.delete(v) } 22 | validator = Seahorse::ParamValidator.new(input_rules) 23 | validator.validate!(params) 24 | rescue ArgumentError => error 25 | if request.headers['HTTP_USER_AGENT'] =~ /sdk|cli/ 26 | service_error(error, 'ValidationError') 27 | else 28 | raise(error) 29 | end 30 | end 31 | 32 | @params = operation.input.from_input(@params) 33 | @params = params.permit(*operation.input.to_strong_params) 34 | 35 | true 36 | end 37 | end 38 | 39 | private 40 | 41 | def render_error(exception) 42 | service_error(exception, exception.class.name) 43 | end 44 | 45 | def params 46 | @params || super 47 | end 48 | 49 | def respond_with(model, opts = {}) 50 | opts[:location] = nil 51 | if opts[:error] 52 | opts[:status] = opts[:error] 53 | super 54 | else 55 | super(operation.output.to_output(model), opts) 56 | end 57 | end 58 | 59 | def operation 60 | return @operation if @operation 61 | @operation = api_model.operation_from_action(action_name) 62 | end 63 | 64 | def api_model 65 | return @api_model if @api_model 66 | @api_model = ('Api::' + controller_name.singularize.camelcase).constantize 67 | end 68 | 69 | def service_error(error, code = 'ServiceError', status = 400) 70 | respond_with({ code: code, message: error.message }, error: status) 71 | end 72 | 73 | def map_headers 74 | return @map_headers if @map_headers 75 | @map_headers = {} 76 | return @map_headers unless operation.input.default_type == 'structure' 77 | operation.input.members.each do |name, member| 78 | if member.header 79 | hdr_name = member.header == true ? name : member.header 80 | hdr_name = "HTTP_" + hdr_name.upcase.gsub('-', '_') 81 | @map_headers[name] = request.headers[hdr_name] 82 | end 83 | end 84 | @map_headers 85 | end 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /lib/seahorse/model.rb: -------------------------------------------------------------------------------- 1 | module Seahorse 2 | module Model 3 | @@apis ||= {} 4 | 5 | class << self 6 | def apis; @@apis end 7 | 8 | def add_all_routes(router) 9 | Dir.glob("#{Rails.root}/app/models/api/*.rb").each {|f| load f } 10 | @@apis.values.each {|api| api.add_routes(router) } 11 | end 12 | end 13 | 14 | extend ActiveSupport::Concern 15 | 16 | included do 17 | @@apis[name.underscore.gsub(/_api$|^api\//, '')] = self 18 | end 19 | 20 | module ClassMethods 21 | attr_reader :operations 22 | 23 | def model_name 24 | name.underscore.gsub(/_api$|^api\//, '') 25 | end 26 | 27 | def add_routes(router) 28 | Seahorse::Router.new(self).add_routes(router) 29 | end 30 | 31 | def desc(text) 32 | @desc = text 33 | end 34 | 35 | def operation(name, &block) 36 | name, action = *operation_name_and_action(name) 37 | @actions ||= {} 38 | @operations ||= {} 39 | @operations[name] = Operation.new(self, name, action, &block) 40 | @operations[name].documentation = @desc 41 | @actions[action] = @operations[name] 42 | @desc = nil 43 | end 44 | 45 | def operation_from_action(action) 46 | @actions ||= {} 47 | @actions[action] 48 | end 49 | 50 | def type(name, &block) 51 | supertype = 'structure' 52 | name, supertype = *name.map {|k,v| [k, v] }.flatten if Hash === name 53 | ShapeBuilder.type(name, supertype, &block) 54 | end 55 | 56 | def to_hash 57 | ops = @operations.inject({}) do |hash, (name, operation)| 58 | hash[name.camelcase(:lower)] = operation.to_hash 59 | hash 60 | end 61 | {'operations' => ops} 62 | end 63 | 64 | private 65 | 66 | def operation_name_and_action(name) 67 | if Hash === name 68 | name.to_a.first.map(&:to_s).reverse 69 | else 70 | case name.to_s 71 | when 'index', 'list' 72 | ["list_#{model_name.pluralize}", 'index'] 73 | when 'show' 74 | ["get_#{model_name}", name.to_s] 75 | else 76 | ["#{name}_#{model_name}", name.to_s] 77 | end 78 | end 79 | end 80 | end 81 | end 82 | end -------------------------------------------------------------------------------- /lib/seahorse/operation.rb: -------------------------------------------------------------------------------- 1 | module Seahorse 2 | class Operation 3 | attr_reader :name, :verb, :action 4 | attr_accessor :documentation 5 | 6 | def initialize(controller, name, action = nil, &block) 7 | @name = name.to_s 8 | @action = action.to_s 9 | @controller = controller 10 | url_prefix = "/" + controller.model_name.pluralize 11 | url_extra = nil 12 | 13 | case action.to_s 14 | when 'index' 15 | @verb = 'get' 16 | when 'show' 17 | @verb = 'get' 18 | url_extra = ':id' 19 | when 'destroy', 'delete' 20 | @verb = 'delete' 21 | url_extra = ':id' 22 | when 'create' 23 | @verb = 'post' 24 | when 'update' 25 | @verb = 'put' 26 | else 27 | @verb = 'get' 28 | url_extra = name.to_s 29 | end 30 | @url = url_prefix + (url_extra ? "/#{url_extra}" : "") 31 | 32 | instance_eval(&block) 33 | end 34 | 35 | def verb(verb = nil) 36 | verb ? (@verb = verb) : @verb 37 | end 38 | 39 | def url(url = nil) 40 | url ? (@url = url) : @url 41 | end 42 | 43 | def input(type = nil, &block) 44 | @input ||= ShapeBuilder.type_class_for(type || 'structure').new 45 | type || block ? ShapeBuilder.new(@input).build(&block) : @input 46 | end 47 | 48 | def output(type = nil, &block) 49 | @output ||= ShapeBuilder.type_class_for(type || 'structure').new 50 | type || block ? ShapeBuilder.new(@output).build(&block) : @output 51 | end 52 | 53 | def to_hash 54 | { 55 | 'name' => name.camelcase, 56 | 'http' => { 57 | 'uri' => url.gsub(/:(\w+)/, '{\1}'), 58 | 'method' => verb.upcase 59 | }, 60 | 'input' => input.to_hash, 61 | 'output' => output.to_hash, 62 | 'documentation' => documentation 63 | } 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /lib/seahorse/param_validator.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2011-2013 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"). You 4 | # may not use this file except in compliance with the License. A copy of 5 | # the License is located at 6 | # 7 | # http://aws.amazon.com/apache2.0/ 8 | # 9 | # or in the "license" file accompanying this file. This file is 10 | # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 11 | # ANY KIND, either express or implied. See the License for the specific 12 | # language governing permissions and limitations under the License. 13 | 14 | require 'date' 15 | require 'time' 16 | require 'pathname' 17 | 18 | module Seahorse 19 | # @api private 20 | class ParamValidator 21 | 22 | # @param [Hash] rules 23 | def initialize rules 24 | @rules = (rules || {})['members'] || {} 25 | end 26 | 27 | # @param [Hash] params A hash of request params. 28 | # @raise [ArgumentError] Raises an `ArgumentError` if any of the given 29 | # request parameters are invalid. 30 | # @return [Boolean] Returns `true` if the `params` are valid. 31 | def validate! params 32 | validate_structure(@rules, params || {}) 33 | true 34 | end 35 | 36 | private 37 | 38 | def validate_structure rules, params, context = "params" 39 | # require params to be a hash 40 | unless params.is_a?(Hash) 41 | raise ArgumentError, "expected a hash for #{context}" 42 | end 43 | 44 | # check for missing required params 45 | rules.each_pair do |param_name, rule| 46 | if rule['required'] 47 | unless params.key?(param_name) or params.key?(param_name.to_sym) 48 | msg = "missing required option :#{param_name} in #{context}" 49 | raise ArgumentError, msg 50 | end 51 | end 52 | end 53 | 54 | # validate hash members 55 | params.each_pair do |param_name, param_value| 56 | if param_rules = rules[param_name.to_s] 57 | member_context = "#{context}[#{param_name.inspect}]" 58 | validate_member(param_rules, param_value, member_context) 59 | else 60 | msg = "unexpected option #{param_name.inspect} found in #{context}" 61 | raise ArgumentError, msg 62 | end 63 | end 64 | end 65 | 66 | def validate_list rules, params, context 67 | # require an array 68 | unless params.is_a?(Array) 69 | raise ArgumentError, "expected an array for #{context}" 70 | end 71 | # validate array members 72 | params.each_with_index do |param_value,n| 73 | validate_member(rules, param_value, context + "[#{n}]") 74 | end 75 | end 76 | 77 | def validate_map rules, params, context 78 | # require params to be a hash 79 | unless params.is_a?(Hash) 80 | raise ArgumentError, "expected a hash for #{context}" 81 | end 82 | # validate hash keys and members 83 | params.each_pair do |key,param_value| 84 | unless key.is_a?(String) 85 | msg = "expected hash keys for #{context} to be strings" 86 | raise ArgumentError, msg 87 | end 88 | validate_member(rules, param_value, context + "[#{key.inspect}]") 89 | end 90 | end 91 | 92 | def validate_member rules, param, context 93 | member_rules = rules['members'] || {} 94 | case rules['type'] 95 | when 'structure' then validate_structure(member_rules, param, context) 96 | when 'list' then validate_list(member_rules, param, context) 97 | when 'map' then validate_map(member_rules, param, context) 98 | else validate_scalar(rules, param, context) 99 | end 100 | end 101 | 102 | def validate_scalar rules, param, context 103 | case rules['type'] 104 | when 'string', nil 105 | unless param.respond_to?(:to_str) 106 | raise ArgumentError, "expected #{context} to be a string" 107 | end 108 | when 'integer' 109 | unless param.respond_to?(:to_int) 110 | raise ArgumentError, "expected #{context} to be an integer" 111 | end 112 | when 'timestamp' 113 | case param 114 | when Time, DateTime, Date, Integer 115 | when /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$/ 116 | else 117 | msg = "expected #{context} to be a Time/DateTime/Date object, " 118 | msg << "an integer or an iso8601 string" 119 | raise ArgumentError, msg 120 | end 121 | when 'boolean' 122 | unless [true,false].include?(param) 123 | raise ArgumentError, "expected #{context} to be a boolean" 124 | end 125 | when 'float' 126 | unless param.is_a?(Numeric) 127 | raise ArgumentError, "expected #{context} to be a Numeric (float)" 128 | end 129 | when 'base64', 'binary' 130 | unless 131 | param.is_a?(String) or 132 | (param.respond_to?(:read) and param.respond_to?(:rewind)) or 133 | param.is_a?(Pathname) 134 | then 135 | msg = "expected #{context} to be a string, an IO object or a " 136 | msg << "Pathname object" 137 | raise ArgumentError, msg 138 | end 139 | else 140 | raise ArgumentError, "unhandled type `#{rules['type']}' for #{context}" 141 | end 142 | end 143 | 144 | class << self 145 | 146 | # @param [Hash] rules 147 | # @param [Hash] params 148 | # @raise [ArgumentError] Raises an `ArgumentError` when one or more 149 | # of the request parameters are invalid. 150 | # @return [Boolean] Returns `true` when params are valid. 151 | def validate! rules, params 152 | ParamValidator.new(rules).validate!(params) 153 | end 154 | 155 | end 156 | 157 | end 158 | end 159 | -------------------------------------------------------------------------------- /lib/seahorse/railtie.rb: -------------------------------------------------------------------------------- 1 | module Seahorse 2 | class Railtie < Rails::Railtie 3 | rake_tasks do 4 | load File.dirname(__FILE__) + '/../tasks/seahorse_tasks.rake' 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/seahorse/router.rb: -------------------------------------------------------------------------------- 1 | module Seahorse 2 | class Router 3 | def initialize(model) 4 | @model = model 5 | end 6 | 7 | def add_routes(router) 8 | operations = @model.operations 9 | controller = @model.model_name.pluralize 10 | operations.each do |name, operation| 11 | router.match "/#{name}" => "#{controller}##{operation.action}", 12 | defaults: { format: 'json' }, 13 | via: [:get, operation.verb.to_sym].uniq 14 | router.match operation.url => "#{controller}##{operation.action}", 15 | defaults: { format: 'json' }, 16 | via: operation.verb 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/seahorse/shape_builder.rb: -------------------------------------------------------------------------------- 1 | require_relative './type' 2 | 3 | module Seahorse 4 | class ShapeBuilder 5 | def self.build_default_types 6 | hash = HashWithIndifferentAccess.new 7 | hash.update string: [StringType, nil], 8 | timestamp: [TimestampType, nil], 9 | integer: [IntegerType, nil], 10 | boolean: [BooleanType, nil], 11 | list: [ListType, nil], 12 | structure: [StructureType, nil] 13 | hash 14 | end 15 | 16 | def self.type(type, supertype = 'structure', &block) 17 | klass = Class.new(type_class_for(supertype)) 18 | klass.type = type 19 | @@types[type] = [klass, block] 20 | end 21 | 22 | def self.type_class_for(type) 23 | @@types[type] ? @@types[type][0] : nil 24 | end 25 | 26 | def initialize(context) 27 | @context = context 28 | @desc = nil 29 | end 30 | 31 | def build(&block) 32 | init_blocks = [] 33 | init_blocks << block if block_given? 34 | 35 | # collect the init block for this type and all of its super types 36 | klass = @context.class 37 | while klass != Type 38 | if block = @@types[klass.type][1] 39 | init_blocks << block 40 | end 41 | klass = klass.superclass 42 | end 43 | 44 | init_blocks.reverse.each do |init_block| 45 | instance_eval(&init_block) 46 | end 47 | end 48 | 49 | def method_missing(type, *args, &block) 50 | if @@types[type] 51 | send_type(type, *args, &block) 52 | else 53 | super 54 | end 55 | end 56 | 57 | def desc(text) 58 | @desc = text 59 | end 60 | 61 | def model(model) 62 | @context.model = model 63 | end 64 | 65 | private 66 | 67 | def send_type(type, *args, &block) 68 | klass, init_block = *@@types[type.to_s] 69 | shape = klass.new(*args) 70 | shape.documentation = @desc 71 | @context.add(shape) 72 | if init_block || block 73 | old_context, @context = @context, shape 74 | instance_eval(&init_block) if init_block 75 | instance_eval(&block) if block 76 | @context = old_context 77 | end 78 | @desc = nil 79 | true 80 | end 81 | 82 | @@types = build_default_types 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /lib/seahorse/type.rb: -------------------------------------------------------------------------------- 1 | module Seahorse 2 | class Type 3 | attr_accessor :name, :required, :model, :location, :header, :uri, :as 4 | attr_accessor :documentation 5 | 6 | def self.type; @type || name.to_s.underscore.gsub(/_type$|^.+\//, '') end 7 | def self.type=(v) @type = v end 8 | def self.inspect; "Type(#{type})" end 9 | 10 | def initialize(*args) 11 | name, opts = nil, {} 12 | if args.size == 0 13 | name = type 14 | elsif args.size == 2 15 | name, opts = args.first, args.last 16 | elsif Hash === args.first 17 | opts = args.first 18 | else 19 | name = args.first 20 | end 21 | 22 | self.name = name.to_s 23 | opts.each {|k, v| send("#{k}=", v) } 24 | end 25 | 26 | def inspect 27 | variables = instance_variables.map do |v| 28 | next if v.to_s =~ /^@(?:(?:default_)?type|name|model)$/ 29 | [v.to_s[1..-1], instance_variable_get(v).inspect].join('=') 30 | end.compact.join(' ') 31 | variables = ' ' + variables if variables.length > 0 32 | "#