eipiai 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: