eipiai 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 877a72bd72e8b2b172a6f1d5e534c938b12dc675
4
+ data.tar.gz: 3b11eae2304fa9a61a2ccb45e78eb0c2a1be17ff
5
+ SHA512:
6
+ metadata.gz: ea7009b9c493cb4e757c5ee6f3a19a0db3ead47df7c5dcd555acf73e01565e97469529a596f031924331821029241d3ccbab328092b0caade6cb7d5c7b896f75
7
+ data.tar.gz: 5a1a3e46d6ed34468c386d1388315a9d2d2235563bbfa1c9b0e00906d97958b7cfa87ef5aecffaec29f8647723bac4e234d8046a637737d0baab520319d37975
data/.gitignore ADDED
@@ -0,0 +1,10 @@
1
+ _builds
2
+ _cache
3
+ _projects
4
+ _steps
5
+ .bundle
6
+ .yardoc
7
+ *.gem
8
+ doc/
9
+ Gemfile*.lock
10
+ pkg/*
data/.hubbit.yml ADDED
File without changes
data/.rubocop.yml ADDED
@@ -0,0 +1,5 @@
1
+ Metrics/LineLength:
2
+ Max: 100
3
+
4
+ Style/AlignParameters:
5
+ EnforcedStyle: with_fixed_indentation
data/.wercker.yml ADDED
@@ -0,0 +1,12 @@
1
+ box: ruby
2
+
3
+ build:
4
+ steps:
5
+ - bundle-install:
6
+ jobs: "8"
7
+ - script:
8
+ name: bootstrap
9
+ code: script/bootstrap
10
+ - script:
11
+ name: tests
12
+ code: script/test
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
data/README.md ADDED
@@ -0,0 +1,84 @@
1
+ # `/ˌeɪ.piˈaɪ/` – The JSON API Framework
2
+
3
+ Opinionated JSON-API stack to get the job done.
4
+
5
+ This library brings some of the _best_\* Ruby JSON API libraries together,
6
+ provides the glue to make things easy to work with out-of-the-box, and even
7
+ easier to modify to your own liking.
8
+
9
+ \* _in our humble opinion_
10
+
11
+ ## Usage
12
+
13
+ More detailed installation instructions will follow. But for now, here's an
14
+ example:
15
+
16
+ ```ruby
17
+ gem 'eipiai'
18
+ gem 'roar'
19
+ gem 'sequel'
20
+ gem 'sqlite3'
21
+ ```
22
+
23
+ ```sh
24
+ bundle install
25
+ ```
26
+
27
+ ```ruby
28
+ require 'eipiai'
29
+ require 'roar/decorator'
30
+ require 'roar/json/hal'
31
+ require 'sequel'
32
+ require 'sqlite3'
33
+
34
+ DB = Sequel.sqlite
35
+ DB.create_table(:items) do
36
+ primary_key :id
37
+ String :uid
38
+ end
39
+
40
+ class Item < Sequel::Model
41
+ end
42
+
43
+ class ItemRepresenter < Roar::Decorator
44
+ include Roar::JSON::HAL
45
+ property :uid
46
+
47
+ def path
48
+ "/item/#{represented.uid}"
49
+ end
50
+ end
51
+
52
+ class ItemsRepresenter < Roar::Decorator
53
+ include Roar::JSON::HAL
54
+ collection :items, getter: proc { Item.all }, decorator: ItemRepresenter
55
+
56
+ def path
57
+ '/items'
58
+ end
59
+ end
60
+
61
+ class ItemResource < Webmachine::Resource
62
+ include Eipiai::Resource
63
+ end
64
+
65
+ class ItemsResource < Webmachine::Resource
66
+ include Eipiai::Resource
67
+ end
68
+
69
+ App = Webmachine::Application.new do |app|
70
+ app.routes do
71
+ add ['items'], ItemsResource
72
+ add ['item', :item_uid], ItemResource
73
+ end
74
+ end
75
+
76
+ App.run
77
+ ```
78
+
79
+ ```sh
80
+ bundle exec ruby app.rb &
81
+ curl -XPOST -d'{"uid":"awesome"}' -HContent-Type:application/json localhost:8080/items
82
+ curl localhost:8080/item/awesome
83
+ # {"uid":"awesome"}
84
+ ```
data/Rakefile ADDED
@@ -0,0 +1,37 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'cucumber/rake/task'
3
+ require 'rubocop/rake_task'
4
+ require 'yard-doctest'
5
+
6
+ task default: %w(rubocop features yard:doctest)
7
+
8
+ RuboCop::RakeTask.new
9
+
10
+ desc 'Update CHANGELOG.md'
11
+ task 'changelog' do
12
+ args = %w(
13
+ --user=blendle
14
+ --project=eipiai
15
+ --header-label="# CHANGELOG"
16
+ --bug-labels=type/bug,bug
17
+ --enhancement-labels=type/enhancement,enhancement
18
+ --future-release=unreleased
19
+ --no-verbose
20
+ )
21
+
22
+ sh %(github_changelog_generator #{args.join(' ')})
23
+ end
24
+
25
+ YARD::Doctest::RakeTask.new do |task|
26
+ task.doctest_opts = %w(--pride)
27
+ end
28
+
29
+ YARD::Rake::YardocTask.new do |task|
30
+ task.files = %w(lib/**/*.rb)
31
+ task.options = %w(--markup markdown --readme README.md)
32
+ end
33
+
34
+ Cucumber::Rake::Task.new(:features) do |t|
35
+ t.cucumber_opts = 'features --format pretty --tags ~@wip --no-source ' \
36
+ '--color --strict --expand --order random'
37
+ end
data/eipiai.gemspec ADDED
@@ -0,0 +1,35 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('lib', __dir__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'eipiai/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'eipiai'
8
+ spec.version = Eipiai::VERSION
9
+ spec.authors = ['Jean Mertz']
10
+ spec.email = %w(jean@mertz.fm)
11
+ spec.summary = 'Opinionated JSON-API stack to get the job done.'
12
+ spec.homepage = 'https://github.com/blendle/eipiai'
13
+
14
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(support)/}) }
15
+ spec.bindir = 'bin'
16
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
17
+ spec.require_paths = %w(lib)
18
+
19
+ spec.add_runtime_dependency 'activesupport'
20
+ spec.add_runtime_dependency 'addressable'
21
+ spec.add_runtime_dependency 'roar'
22
+ spec.add_runtime_dependency 'webmachine'
23
+
24
+ spec.add_development_dependency 'bundler'
25
+ spec.add_development_dependency 'cucumber'
26
+ spec.add_development_dependency 'cucumber-blendle-steps'
27
+ spec.add_development_dependency 'database_cleaner'
28
+ spec.add_development_dependency 'pry'
29
+ spec.add_development_dependency 'rack-test'
30
+ spec.add_development_dependency 'rack'
31
+ spec.add_development_dependency 'rake'
32
+ spec.add_development_dependency 'rubocop'
33
+ spec.add_development_dependency 'sqlite3'
34
+ spec.add_development_dependency 'yard-doctest'
35
+ end
@@ -0,0 +1,27 @@
1
+ lib = File.expand_path('../lib', __dir__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+
4
+ require 'eipiai'
5
+ require 'webmachine/adapters/rack'
6
+ require_relative '../../support/fixtures/webmachine/app'
7
+
8
+ include Eipiai::Webmachine::TestApp
9
+
10
+ App = Webmachine::Application.new do |app|
11
+ app.routes do
12
+ add ['api'], Eipiai::ApiResource
13
+
14
+ add ['items'], ItemsResource
15
+ add ['item', :item_uid], ItemResource
16
+ end
17
+
18
+ app.configure do |config|
19
+ config.adapter = :Rack
20
+ end
21
+
22
+ Eipiai::ApiRepresenter.populate_from_app(app)
23
+ end
24
+
25
+ def app
26
+ App.adapter
27
+ end
@@ -0,0 +1,8 @@
1
+ require 'database_cleaner'
2
+ require 'database_cleaner/cucumber'
3
+
4
+ DatabaseCleaner.clean_with(:truncation)
5
+ DatabaseCleaner.strategy = :transaction
6
+
7
+ Before { DatabaseCleaner.start }
8
+ After { DatabaseCleaner.clean }
@@ -0,0 +1,6 @@
1
+ require 'cucumber/blendle_steps'
2
+
3
+ Before do
4
+ env('HTTPS', 'on')
5
+ env('SERVER_PORT', '443')
6
+ end
@@ -0,0 +1,134 @@
1
+ Feature: Webmachine CRUD operations
2
+
3
+ When a new JSON API service needs to be developed
4
+ I want to use Eipiai's default CRUD setup
5
+ So that the basic API operations are quick and easyi to implement
6
+
7
+ Scenario: List main API endpoint
8
+ When the client does a GET request to "/api"
9
+ Then the status code should be "200" (OK)
10
+ And the response should be HAL/JSON:
11
+ """json
12
+ {
13
+ "_links": {
14
+ "self": {
15
+ "href": "https://example.org/api"
16
+ },
17
+ "api": {
18
+ "href": "https://example.org/api"
19
+ },
20
+ "items": {
21
+ "href": "https://example.org/items"
22
+ },
23
+ "item": {
24
+ "templated": true,
25
+ "href": "https://example.org/item/{item_uid}"
26
+ }
27
+ }
28
+ }
29
+ """
30
+
31
+ Scenario: Create resource
32
+ Given the client provides the header "Content-Type: application/json"
33
+ When the client does a POST request to the "items" resource with the following content:
34
+ """json
35
+ {
36
+ "uid": "hello",
37
+ "price": 10
38
+ }
39
+ """
40
+ Then the status code should be "201" (Created)
41
+ And the response contains the "Location" header with value "https://example.org/item/hello"
42
+ And the response should be HAL/JSON:
43
+ """json
44
+ {
45
+ "_links": {
46
+ "self": {
47
+ "href": "https://example.org/item/hello"
48
+ }
49
+ },
50
+ "uid": "hello",
51
+ "price": 10
52
+ }
53
+ """
54
+
55
+ Scenario: Get resource
56
+ Given the item with uid "hello" and the price "10" exists
57
+ When the client does a GET request to the "item" resource with the template variable "item_uid" set to "hello"
58
+ Then the status code should be "200" (OK)
59
+ And the response should be HAL/JSON:
60
+ """json
61
+ {
62
+ "_links": {
63
+ "self": {
64
+ "href": "https://example.org/item/hello"
65
+ }
66
+ },
67
+ "uid": "hello",
68
+ "price": 10
69
+ }
70
+ """
71
+
72
+ Scenario: Update resource
73
+ Given the item with uid "hello" and the price "10" exists
74
+ Given the client provides the header "Content-Type: application/json"
75
+ When the client does a PUT request to the "item" resource with the template variable "item_uid" set to "hello" and the following content:
76
+ """json
77
+ {
78
+ "price": 100
79
+ }
80
+ """
81
+ Then the status code should be "204" (No Content)
82
+ And the response contains the "Location" header with value "https://example.org/item/hello"
83
+
84
+ When the client does a GET request to the "item" resource with the template variable "item_uid" set to "hello"
85
+ Then the status code should be "200" (OK)
86
+ And the response should be HAL/JSON:
87
+ """json
88
+ {
89
+ "_links": {
90
+ "self": {
91
+ "href": "https://example.org/item/hello"
92
+ }
93
+ },
94
+ "uid": "hello",
95
+ "price": 100
96
+ }
97
+ """
98
+
99
+ Scenario: List resources
100
+ Given the item with uid "hello" and the price "10" exists
101
+ When the client does a GET request to the "items" resource
102
+ Then the status code should be "200" (OK)
103
+ And the response should be HAL/JSON:
104
+ """json
105
+ {
106
+ "_links": {
107
+ "self": {
108
+ "href": "https://example.org/items"
109
+ },
110
+ "b:items": [
111
+ {
112
+ "href": "https://example.org/item/hello"
113
+ }
114
+ ]
115
+ },
116
+ "b:items": [
117
+ {
118
+ "_links": {
119
+ "self": {
120
+ "href": "https://example.org/item/hello"
121
+ }
122
+ },
123
+ "uid": "hello",
124
+ "price": 10
125
+ }
126
+ ]
127
+ }
128
+ """
129
+
130
+ Scenario: Delete resource
131
+ Given the item with uid "hello" exists
132
+ When the client does a DELETE request to the "item" resource with the template variable "item_uid" set to "hello"
133
+ Then the status code should be "204" (No Content)
134
+ And the item with uid "hello" should not exist
@@ -0,0 +1,44 @@
1
+ require 'addressable/uri'
2
+ require 'roar'
3
+ require 'roar/decorator'
4
+ require 'roar/json/hal'
5
+
6
+ module Eipiai
7
+ # ApiRepresenter
8
+ #
9
+ # This representer returns an object that represents the `/api` endpoint of
10
+ # the API. It contains all routes as defined in the Webmachine route, and also
11
+ # tells the client if the route is templated or not.
12
+ #
13
+ class ApiRepresenter < Roar::Decorator
14
+ include Roar::JSON::HAL
15
+
16
+ link(:self) do |request|
17
+ request.present? ? Addressable::URI.parse(request.uri).merge(path: path).to_s : path
18
+ end
19
+
20
+ def self.populate_from_app(app)
21
+ app.routes.each do |route|
22
+ next unless route.resource.respond_to?(:link_identifier)
23
+
24
+ templated = route.path_spec.any? { |r| r.is_a?(Symbol) }
25
+ options = { rel: route.resource.link_identifier }
26
+ options.merge!(templated: templated) if templated
27
+
28
+ add_route(route, options)
29
+ end
30
+ end
31
+
32
+ def self.add_route(route, options)
33
+ link(options) do |request|
34
+ path = '/' + route.path_spec.map { |e| e.is_a?(Symbol) ? "{#{e}}" : e }.join('/')
35
+
36
+ request.present? ? Addressable::URI.parse(request.uri).merge(path: path).to_s : path
37
+ end
38
+ end
39
+
40
+ def path
41
+ '/api'
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,3 @@
1
+ require 'roar'
2
+
3
+ require 'eipiai/roar/representers/api'
@@ -0,0 +1,7 @@
1
+ # Eipiai
2
+ #
3
+ # The current version of the Eipiai library.
4
+ #
5
+ module Eipiai
6
+ VERSION = '0.1.0'
7
+ end
@@ -0,0 +1,66 @@
1
+ require 'webmachine'
2
+
3
+ module Webmachine
4
+ class Resource
5
+ # Callbacks
6
+ #
7
+ # @see http://git.io/v3vYg
8
+ #
9
+ module Callbacks
10
+ # unprocessable_entity?
11
+ #
12
+ # If the request is semantically correct, but unprocessable due to invalid
13
+ # data, this should return true, which will result in a '422 Unprocessable
14
+ # Entity' response.
15
+ #
16
+ # Defaults to false.
17
+ #
18
+ # @return [true, false] whether the request is unprocessable
19
+ #
20
+ def unprocessable_entity?
21
+ false
22
+ end
23
+
24
+ # payment_required?
25
+ #
26
+ # If payment is required this will result in a '402 Payment Required'
27
+ # response.
28
+ #
29
+ # Defaults to false
30
+ #
31
+ # @return [true, false] whether the request requires a payment
32
+ #
33
+ def payment_required?
34
+ false
35
+ end
36
+ end
37
+ end
38
+
39
+ module Decision
40
+ # Flow
41
+ #
42
+ # @see http://git.io/v3vYP
43
+ #
44
+ module Flow
45
+ # Malformed?
46
+ def b9b
47
+ decision_test(resource.malformed_request?, 400, :b9c)
48
+ end
49
+
50
+ # Unprocessable Entity?
51
+ def b9c
52
+ decision_test(resource.unprocessable_entity?, 422, :b8)
53
+ end
54
+
55
+ # Forbidden?
56
+ def b7
57
+ decision_test(resource.forbidden?, 403, :b7a)
58
+ end
59
+
60
+ # Payment Required?
61
+ def b7a
62
+ decision_test(resource.payment_required?, 402, :b6)
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,37 @@
1
+ require 'addressable/uri'
2
+ require 'webmachine'
3
+
4
+ module Webmachine
5
+ # Request
6
+ #
7
+ # @see http://git.io/vWZFi
8
+ #
9
+ class Request
10
+ # URIReplacement
11
+ #
12
+ # Swaps `Webmachine::Request#uri` from `URI` to `Addressable::URI`.
13
+ #
14
+ module URIReplacement
15
+ # build_uri
16
+ #
17
+ # Given an uri object, and headers, calls `Webmachine::Request#build_uri`
18
+ # and parses the result using Addressable (if available), or simply
19
+ # returns the result as-is.
20
+ #
21
+ # @example
22
+ # get('/items').build_uri(URI('http://example.org'), {}).class # => Addressable::URI
23
+ #
24
+ # @param [URI] uri object
25
+ # @param [hash] headers
26
+ #
27
+ # @return [Addressable::URI, URI]
28
+ # Addressable object, or URI if the addressable object is not loaded
29
+ #
30
+ def build_uri(uri, headers)
31
+ defined?(Addressable) ? Addressable::URI.parse(super) : super
32
+ end
33
+ end
34
+
35
+ prepend URIReplacement
36
+ end
37
+ end
@@ -0,0 +1,31 @@
1
+ require 'eipiai/webmachine/resources/base'
2
+
3
+ require 'active_support/core_ext/string/inflections'
4
+ require 'json'
5
+
6
+ module Eipiai
7
+ class Api
8
+ end
9
+
10
+ # ApiResource
11
+ #
12
+ # The base resource which can be included in regular Webmachine::Resource
13
+ # objects. It provides sensible defaults for a full-features REST API
14
+ # endpoint.
15
+ #
16
+ class ApiResource < Webmachine::Resource
17
+ include Resource
18
+
19
+ def allowed_methods
20
+ %w(GET)
21
+ end
22
+
23
+ def cache_header
24
+ 'public, max-age=600, s-maxage=86400'
25
+ end
26
+
27
+ def object
28
+ Api.new
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,196 @@
1
+ require 'eipiai/webmachine/resources/singular'
2
+ require 'eipiai/webmachine/resources/collection'
3
+
4
+ require 'active_support/core_ext/string/inflections'
5
+ require 'json'
6
+
7
+ module Eipiai
8
+ # Resource
9
+ #
10
+ # The base resource which can be included in regular Webmachine::Resource
11
+ # objects. It provides sensible defaults for a full-features REST API
12
+ # endpoint.
13
+ #
14
+ module Resource
15
+ # Includes the correct resource into the class, depending on its name.
16
+ #
17
+ # If the resource is called `ItemResource`, the `Item` part is considered
18
+ # singular, and thus the `SingularResource` is included into the class.
19
+ #
20
+ def self.included(base)
21
+ subject_class_name = base.name.demodulize.chomp('Resource')
22
+
23
+ if subject_class_name == subject_class_name.singularize
24
+ base.send(:include, SingularResource)
25
+ else
26
+ base.send(:include, CollectionResource)
27
+ end
28
+
29
+ base.extend(ClassMethods)
30
+ end
31
+
32
+ def content_types_provided
33
+ [['application/hal+json', :to_json]]
34
+ end
35
+
36
+ def content_types_accepted
37
+ [['application/json', :from_json]]
38
+ end
39
+
40
+ # new_object
41
+ #
42
+ # Return the instantiated object, based on the representer provided by
43
+ # `singular_representer_class` and the parameters provided by `params`.
44
+ #
45
+ # See `Object#new_object` for the version without using representers.
46
+ #
47
+ # @example
48
+ # resource.new_object.class # => Item
49
+ #
50
+ # @return [Class, nil] instantiated representer, or nil if not found
51
+ #
52
+ def new_object
53
+ return if singular_representer_class.nil? || object_class.nil?
54
+
55
+ @new_object ||= singular_representer_class.new(object_class.new).from_hash(params)
56
+ end
57
+
58
+ # params
59
+ #
60
+ # Returns a Hash object, representing the JSOM payload sent in the request
61
+ # object. Returns an empty hash if request body contains invalid JSON.
62
+ #
63
+ # @example Parse valid JSON request
64
+ # resource.send :params, ('{ "hello": "world" }') # => { 'hello' => 'world' }
65
+ #
66
+ # @example Parse invalid JSON request
67
+ # resource.send :params, 'invalid' #=> {}
68
+ #
69
+ # @param [String] body JSON provided as a string
70
+ # @return [Hash] JSON string, converted to a hash
71
+ #
72
+ def params(body = request.body.to_s)
73
+ @params ||= JSON.parse(body)
74
+ rescue JSON::ParserError
75
+ {}
76
+ end
77
+
78
+ # to_hash
79
+ #
80
+ # Given an object, calls `#to_hash` on that object,
81
+ #
82
+ # If the object's `to_hash` implementation accepts any arguments, the
83
+ # `request` object is sent as its first argument.
84
+ #
85
+ # In practice, this method is used without any parameters, causing the
86
+ # method to call `represented`, which represents a Roar representer. This in
87
+ # turn converts the represented object to a HAL/JSON compatible hash
88
+ # representation.
89
+ #
90
+ # @example
91
+ # item = Item.new(uid: 'hello')
92
+ # resource.to_hash(item) # => { uid: 'hello' }
93
+ #
94
+ # @param [Object] o = represented to call #to_hash on
95
+ # @return [Hash] hash representation of the object
96
+ #
97
+ def to_hash(o = represented)
98
+ o.method(:to_hash).arity.zero? ? o.to_hash : o.to_hash(request)
99
+ end
100
+
101
+ def to_json
102
+ to_hash.to_json
103
+ end
104
+
105
+ private
106
+
107
+ # object_class_name
108
+ #
109
+ # String representation of the "object class", based on the current
110
+ # resource name.
111
+ #
112
+ # If the resource is called `ItemsResource`, object_class_name will be
113
+ # `Item`. It is expected that this name represents the class of the object
114
+ # the resource provides access to through the API.
115
+ #
116
+ # @example
117
+ # resource.send :object_class_name # => 'Item'
118
+ #
119
+ # @return [String] name of object class
120
+ #
121
+ def object_class_name
122
+ self.class.name.chomp('Resource').demodulize.singularize
123
+ end
124
+
125
+ # object_class
126
+ #
127
+ # Same as `object_class_name`, except this method will return a constant
128
+ # _and_ keeps the namespace intact.
129
+ #
130
+ # Returns `nil` if constant does not exist.
131
+ #
132
+ # @example
133
+ # resource.send :object_class # => Item
134
+ #
135
+ # @return [Class, nil] object class, or nil if not found
136
+ #
137
+ def object_class
138
+ self.class.name.chomp('Resource').singularize.constantize
139
+ rescue NameError
140
+ nil
141
+ end
142
+
143
+ # singular_representer_class
144
+ #
145
+ # Constant, representing the representer belonging to the defined
146
+ # `object`.
147
+ #
148
+ # If the resource is called `ItemResource`, the representer will be
149
+ # `ItemRepresenter`.
150
+ #
151
+ # Returns `nil` if constant does not exist.
152
+ #
153
+ # @example
154
+ # resource.send :singular_representer_class # => ItemRepresenter
155
+ #
156
+ # @return [Class, nil] representer class, or nil if not found
157
+ #
158
+ def singular_representer_class
159
+ "#{object_class}Representer".constantize
160
+ rescue NameError
161
+ nil
162
+ end
163
+
164
+ # collection_representer_class
165
+ #
166
+ # Constant, representing the representer belonging to the defined
167
+ # collection of `object`s.
168
+ #
169
+ # If the resource is called `ItemResource`, the representer will be
170
+ # `ItemsRepresenter`.
171
+ #
172
+ # Returns `nil` if constant does not exist.
173
+ #
174
+ # @example
175
+ # resource.send :collection_representer_class # => ItemsRepresenter
176
+ #
177
+ # @return [Class, nil] representer class, or nil if not found
178
+ #
179
+ def collection_representer_class
180
+ "#{object_class.to_s.pluralize}Representer".constantize
181
+ rescue NameError
182
+ nil
183
+ end
184
+
185
+ # ClassMethods
186
+ #
187
+ # These methods will be defined on the class in which this module is
188
+ # included.
189
+ #
190
+ module ClassMethods
191
+ def link_identifier
192
+ name.chomp('Resource').demodulize.underscore.to_sym
193
+ end
194
+ end
195
+ end
196
+ end
@@ -0,0 +1,62 @@
1
+ require 'eipiai/webmachine/resources/base'
2
+
3
+ module Eipiai
4
+ # CollectionResource
5
+ #
6
+ # The collection resource is the basis for a resource representing a
7
+ # collection of objects.
8
+ #
9
+ # It provides basic GET and POST support.
10
+ #
11
+ # It is the collection version of the `SingularResource` resource.
12
+ #
13
+ module CollectionResource
14
+ def allowed_methods
15
+ %w(GET POST)
16
+ end
17
+
18
+ # TODO(JeanMertz): enable when validators are implemented.
19
+ #
20
+ # def unprocessable_entity?
21
+ # super && json_error_body(*new_object.errors)
22
+ # end
23
+
24
+ def post_is_create?
25
+ true
26
+ end
27
+
28
+ def create_path
29
+ singular_representer_class.new(new_object).path
30
+ end
31
+
32
+ def from_json
33
+ new_object.save && handle_post_response
34
+ end
35
+
36
+ private
37
+
38
+ def content_type_handler
39
+ content_type = response.headers[::Webmachine::CONTENT_TYPE]
40
+ media_type = ::Webmachine::MediaType.parse(content_type)
41
+
42
+ content_types_provided.find { |ct, _| media_type.type_matches?(ct) }.last
43
+ end
44
+
45
+ def handle_post_response
46
+ response.body = send(content_type_handler)
47
+ true
48
+ end
49
+
50
+ def object
51
+ @object ||= Struct.new(:dataset).new(object_class.dataset) if object_class
52
+ end
53
+
54
+ def representer_class
55
+ request.get? ? collection_representer_class : singular_representer_class
56
+ end
57
+
58
+ def represented
59
+ representer_class.new(request.get? ? object : new_object)
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,66 @@
1
+ require 'eipiai/webmachine/resources/base'
2
+
3
+ require 'active_support/core_ext/object/blank'
4
+ require 'active_support/core_ext/string/inflections'
5
+ require 'json'
6
+
7
+ module Eipiai
8
+ # SingularResource
9
+ #
10
+ # The singular resource is the basis for a resource representing a single
11
+ # object.
12
+ #
13
+ # It provides basic GET, PUT and DELETE support.
14
+ #
15
+ # It is the singular version of the `CollectionResource` resource.
16
+ #
17
+ module SingularResource
18
+ def allowed_methods
19
+ %w(GET PUT DELETE)
20
+ end
21
+
22
+ # TODO(JeanMertz): enable when validators are implemented.
23
+ #
24
+ # def unprocessable_entity?
25
+ # return unless super
26
+ # return if request.put? && new_object.errors.include?(:resource_exists)
27
+ #
28
+ # json_error_body(*new_object.errors)
29
+ # end
30
+
31
+ def resource_exists?
32
+ object.present?
33
+ end
34
+
35
+ def delete_resource
36
+ object.destroy
37
+ true
38
+ end
39
+
40
+ def from_json
41
+ delete_resource if (exists = object.present?)
42
+ new_object.save
43
+
44
+ response.headers['Location'] = request.uri.to_s
45
+ exists ? 204 : 201
46
+ end
47
+
48
+ private
49
+
50
+ def params
51
+ @merged_params ||= super.merge('uid' => object_uid)
52
+ end
53
+
54
+ def represented
55
+ singular_representer_class.new(object)
56
+ end
57
+
58
+ def object
59
+ @object ||= object_class.first(uid: object_uid) if object_class
60
+ end
61
+
62
+ def object_uid
63
+ request.path_info[:"#{object_class_name.downcase}_uid"]
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,9 @@
1
+ require 'webmachine'
2
+
3
+ require 'eipiai/webmachine/ext/decision'
4
+ require 'eipiai/webmachine/ext/request'
5
+
6
+ require 'eipiai/webmachine/resources/api'
7
+ require 'eipiai/webmachine/resources/base'
8
+ require 'eipiai/webmachine/resources/collection'
9
+ require 'eipiai/webmachine/resources/singular'
data/lib/eipiai.rb ADDED
@@ -0,0 +1,2 @@
1
+ require 'eipiai/roar'
2
+ require 'eipiai/webmachine'
data/script/bootstrap ADDED
@@ -0,0 +1,5 @@
1
+ #!/bin/sh
2
+
3
+ bundle check || bundle install
4
+ bundle exec yard config load_plugins true
5
+ bundle exec yard config -a autoload_plugins yard-doctest
@@ -0,0 +1,4 @@
1
+ #!/bin/sh
2
+
3
+ bundle exec rake yard
4
+ open doc/index.html
data/script/test ADDED
@@ -0,0 +1,3 @@
1
+ #!/bin/sh
2
+
3
+ bundle exec rake
metadata ADDED
@@ -0,0 +1,280 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: eipiai
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Jean Mertz
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-12-12 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: addressable
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: roar
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: webmachine
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: bundler
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: cucumber
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: cucumber-blendle-steps
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: database_cleaner
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: pry
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: rack-test
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
153
+ - !ruby/object:Gem::Dependency
154
+ name: rack
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - ">="
158
+ - !ruby/object:Gem::Version
159
+ version: '0'
160
+ type: :development
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - ">="
165
+ - !ruby/object:Gem::Version
166
+ version: '0'
167
+ - !ruby/object:Gem::Dependency
168
+ name: rake
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - ">="
172
+ - !ruby/object:Gem::Version
173
+ version: '0'
174
+ type: :development
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - ">="
179
+ - !ruby/object:Gem::Version
180
+ version: '0'
181
+ - !ruby/object:Gem::Dependency
182
+ name: rubocop
183
+ requirement: !ruby/object:Gem::Requirement
184
+ requirements:
185
+ - - ">="
186
+ - !ruby/object:Gem::Version
187
+ version: '0'
188
+ type: :development
189
+ prerelease: false
190
+ version_requirements: !ruby/object:Gem::Requirement
191
+ requirements:
192
+ - - ">="
193
+ - !ruby/object:Gem::Version
194
+ version: '0'
195
+ - !ruby/object:Gem::Dependency
196
+ name: sqlite3
197
+ requirement: !ruby/object:Gem::Requirement
198
+ requirements:
199
+ - - ">="
200
+ - !ruby/object:Gem::Version
201
+ version: '0'
202
+ type: :development
203
+ prerelease: false
204
+ version_requirements: !ruby/object:Gem::Requirement
205
+ requirements:
206
+ - - ">="
207
+ - !ruby/object:Gem::Version
208
+ version: '0'
209
+ - !ruby/object:Gem::Dependency
210
+ name: yard-doctest
211
+ requirement: !ruby/object:Gem::Requirement
212
+ requirements:
213
+ - - ">="
214
+ - !ruby/object:Gem::Version
215
+ version: '0'
216
+ type: :development
217
+ prerelease: false
218
+ version_requirements: !ruby/object:Gem::Requirement
219
+ requirements:
220
+ - - ">="
221
+ - !ruby/object:Gem::Version
222
+ version: '0'
223
+ description:
224
+ email:
225
+ - jean@mertz.fm
226
+ executables: []
227
+ extensions: []
228
+ extra_rdoc_files: []
229
+ files:
230
+ - ".gitignore"
231
+ - ".hubbit.yml"
232
+ - ".rubocop.yml"
233
+ - ".wercker.yml"
234
+ - Gemfile
235
+ - README.md
236
+ - Rakefile
237
+ - eipiai.gemspec
238
+ - features/support/app.rb
239
+ - features/support/db.rb
240
+ - features/support/env.rb
241
+ - features/webmachine.feature
242
+ - lib/eipiai.rb
243
+ - lib/eipiai/roar.rb
244
+ - lib/eipiai/roar/representers/api.rb
245
+ - lib/eipiai/version.rb
246
+ - lib/eipiai/webmachine.rb
247
+ - lib/eipiai/webmachine/ext/decision.rb
248
+ - lib/eipiai/webmachine/ext/request.rb
249
+ - lib/eipiai/webmachine/resources/api.rb
250
+ - lib/eipiai/webmachine/resources/base.rb
251
+ - lib/eipiai/webmachine/resources/collection.rb
252
+ - lib/eipiai/webmachine/resources/singular.rb
253
+ - script/bootstrap
254
+ - script/documentation
255
+ - script/test
256
+ homepage: https://github.com/blendle/eipiai
257
+ licenses: []
258
+ metadata: {}
259
+ post_install_message:
260
+ rdoc_options: []
261
+ require_paths:
262
+ - lib
263
+ required_ruby_version: !ruby/object:Gem::Requirement
264
+ requirements:
265
+ - - ">="
266
+ - !ruby/object:Gem::Version
267
+ version: '0'
268
+ required_rubygems_version: !ruby/object:Gem::Requirement
269
+ requirements:
270
+ - - ">="
271
+ - !ruby/object:Gem::Version
272
+ version: '0'
273
+ requirements: []
274
+ rubyforge_project:
275
+ rubygems_version: 2.4.5.1
276
+ signing_key:
277
+ specification_version: 4
278
+ summary: Opinionated JSON-API stack to get the job done.
279
+ test_files: []
280
+ has_rdoc: