jsonapi-resources 0.0.1

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.
data/Rakefile ADDED
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
3
+ require "rake/testtask"
4
+
5
+ desc 'Run tests'
6
+ test_task = Rake::TestTask.new(:test) do |t|
7
+ t.libs << 'test'
8
+ t.pattern = 'test/**/*_test.rb'
9
+ t.verbose = true
10
+ end
11
+
12
+ task default: :test
13
+
14
+ desc 'Run tests in isolated processes'
15
+ namespace :test do
16
+ task :isolated do
17
+ Dir[test_task.pattern].each do |file|
18
+ cmd = ['ruby']
19
+ test_task.libs.each { |l| cmd << '-I' << l }
20
+ cmd << file
21
+ sh cmd.join(' ')
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,29 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'jsonapi/resources/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'jsonapi-resources'
8
+ spec.version = JSONAPI::Resources::VERSION
9
+ spec.authors = ['Dan Gebhardt', 'Larry Gebhardt']
10
+ spec.email = ['dan@cerebris.com', 'larry@cerebris.com']
11
+ spec.summary = %q{Easily support JSON API in Rails.}
12
+ spec.description = %q{A resource-centric approach to implementing the controllers, routes, and serializers needed to support the JSON API spec.}
13
+ spec.homepage = 'https://github.com/cerebris/jsonapi-resources'
14
+ spec.license = 'MIT'
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ['lib']
20
+ spec.required_ruby_version = '>= 1.9.3'
21
+
22
+ spec.add_development_dependency 'bundler', '~> 1.5'
23
+ spec.add_development_dependency 'rake'
24
+ spec.add_development_dependency 'minitest'
25
+ spec.add_development_dependency 'minitest-spec-rails'
26
+ spec.add_development_dependency 'minitest-reporters'
27
+ spec.add_development_dependency 'simplecov'
28
+ spec.add_development_dependency 'rails', '>= 4.0'
29
+ end
@@ -0,0 +1,2 @@
1
+ require 'jsonapi/resource'
2
+ require 'jsonapi/resources/version'
@@ -0,0 +1,17 @@
1
+ require 'jsonapi/operations_processor'
2
+
3
+ module JSONAPI
4
+ class ActiveRecordOperationsProcessor < OperationsProcessor
5
+
6
+ private
7
+ def transaction
8
+ ActiveRecord::Base.transaction do
9
+ yield
10
+ end
11
+ end
12
+
13
+ def rollback
14
+ raise ActiveRecord::Rollback
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,45 @@
1
+ module JSONAPI
2
+ class Association
3
+ def initialize(name, options={})
4
+ @name = name.to_s
5
+ @options = options
6
+ @key = options[:key] ? options[:key].to_sym : nil
7
+ @primary_key = options.fetch(:primary_key, 'id').to_sym
8
+ @treat_as_set = options.fetch(:treat_as_set, false) == true
9
+ end
10
+
11
+ def key
12
+ @key
13
+ end
14
+
15
+ def primary_key
16
+ @primary_key
17
+ end
18
+
19
+ def treat_as_set
20
+ @treat_as_set
21
+ end
22
+
23
+ def serialize_type_name
24
+ @serialize_type_name
25
+ end
26
+
27
+ class HasOne < Association
28
+ def initialize(name, options={})
29
+ super
30
+ class_name = options.fetch(:class_name, name.to_s.capitalize)
31
+ @serialize_type_name = class_name.underscore.pluralize.to_sym
32
+ @key ||= "#{name}_id".to_sym
33
+ end
34
+ end
35
+
36
+ class HasMany < Association
37
+ def initialize(name, options={})
38
+ super
39
+ class_name = options.fetch(:class_name, name.to_s.capitalize.singularize).to_sym
40
+ @serialize_type_name = class_name.to_s.underscore.pluralize.to_sym
41
+ @key ||= "#{name.to_s.singularize}_ids".to_sym
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,17 @@
1
+ module JSONAPI
2
+ class Error
3
+
4
+ attr_accessor :title, :detail, :id, :href, :code, :path, :links, :status
5
+
6
+ def initialize(options={})
7
+ @title = options[:title]
8
+ @detail = options[:detail]
9
+ @id = options[:id]
10
+ @href = options[:href]
11
+ @code = options[:code]
12
+ @path = options[:path]
13
+ @links = options[:links]
14
+ @status = options[:status]
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,16 @@
1
+ module JSONAPI
2
+ VALIDATION_ERROR = 100
3
+ INVALID_RESOURCE = 101
4
+ FILTER_NOT_ALLOWED = 102
5
+ INVALID_FIELD_VALUE = 103
6
+ INVALID_FIELD = 104
7
+ PARAM_NOT_ALLOWED = 105
8
+ PARAM_MISSING = 106
9
+ INVALID_FILTER_VALUE = 107
10
+ COUNT_MISMATCH = 108
11
+ KEY_ORDER_MISMATCH = 109
12
+ KEY_NOT_INCLUDED_IN_URL = 110
13
+
14
+ RECORD_NOT_FOUND = 404
15
+ LOCKED = 423
16
+ end
@@ -0,0 +1,177 @@
1
+ module JSONAPI
2
+ module Exceptions
3
+ class Error < RuntimeError; end
4
+
5
+ class InvalidResource < Error
6
+ attr_accessor :resource
7
+ def initialize(resource)
8
+ @resource = resource
9
+ end
10
+
11
+ def errors
12
+ [JSONAPI::Error.new(code: JSONAPI::INVALID_RESOURCE,
13
+ status: :bad_request,
14
+ title: 'Invalid resource',
15
+ detail: "#{resource} is not a valid resource.")]
16
+ end
17
+ end
18
+
19
+ class RecordNotFound < Error
20
+ attr_accessor :id
21
+ def initialize(id)
22
+ @id = id
23
+ end
24
+
25
+ def errors
26
+ [JSONAPI::Error.new(code: JSONAPI::RECORD_NOT_FOUND,
27
+ status: :not_found,
28
+ title: 'Record not found',
29
+ detail: "The record identified by #{id} could not be found.")]
30
+ end
31
+ end
32
+
33
+ class FilterNotAllowed < Error
34
+ attr_accessor :filter
35
+ def initialize(filter)
36
+ @filter = filter
37
+ end
38
+
39
+ def errors
40
+ [JSONAPI::Error.new(code: JSONAPI::FILTER_NOT_ALLOWED,
41
+ status: :bad_request,
42
+ title: 'Filter not allowed',
43
+ detail: "#{filter} is not allowed.")]
44
+ end
45
+ end
46
+
47
+ class InvalidFilterValue < Error
48
+ attr_accessor :filter, :value
49
+ def initialize(filter, value)
50
+ @filter = filter
51
+ @value = value
52
+ end
53
+
54
+ def errors
55
+ [JSONAPI::Error.new(code: JSONAPI::INVALID_FILTER_VALUE,
56
+ status: :bad_request,
57
+ title: 'Invalid filter value',
58
+ detail: "#{value} is not a valid value for #{filter}.")]
59
+ end
60
+ end
61
+
62
+ class InvalidFieldValue < Error
63
+ attr_accessor :field, :value
64
+ def initialize(field, value)
65
+ @field = field
66
+ @value = value
67
+ end
68
+
69
+ def errors
70
+ [JSONAPI::Error.new(code: JSONAPI::INVALID_FIELD_VALUE,
71
+ status: :bad_request,
72
+ title: 'Invalid field value',
73
+ detail: "#{value} is not a valid value for #{field}.")]
74
+ end
75
+ end
76
+
77
+ class InvalidField < Error
78
+ attr_accessor :field, :type
79
+ def initialize(type, field)
80
+ @field = field
81
+ @type = type
82
+ end
83
+
84
+ def errors
85
+ [JSONAPI::Error.new(code: JSONAPI::INVALID_FIELD,
86
+ status: :bad_request,
87
+ title: 'Invalid field',
88
+ detail: "#{field} is not a valid field for #{type}.")]
89
+ end
90
+ end
91
+
92
+ class ParametersNotAllowed < Error
93
+ attr_accessor :params
94
+ def initialize(params)
95
+ @params = params
96
+ end
97
+
98
+ def errors
99
+ params.collect { |param|
100
+ JSONAPI::Error.new(code: JSONAPI::PARAM_NOT_ALLOWED,
101
+ status: :bad_request,
102
+ title: 'Param not allowed',
103
+ detail: "#{param} is not allowed.")
104
+ }
105
+
106
+ end
107
+ end
108
+
109
+ class ParameterMissing < Error
110
+ attr_accessor :param
111
+ def initialize(param)
112
+ @param = param
113
+ end
114
+
115
+ def errors
116
+ [JSONAPI::Error.new(code: JSONAPI::PARAM_MISSING,
117
+ status: :bad_request,
118
+ title: 'Missing Parameter',
119
+ detail: "The required parameter, #{param}, is missing.")]
120
+ end
121
+ end
122
+
123
+ class CountMismatch < Error
124
+ def errors
125
+ [JSONAPI::Error.new(code: JSONAPI::COUNT_MISMATCH,
126
+ status: :bad_request,
127
+ title: 'Count to key mismatch',
128
+ detail: 'The resource collection does not contain the same number of objects as the number of keys.')]
129
+ end
130
+ end
131
+
132
+ class KeyNotIncludedInURL < Error
133
+ attr_accessor :key
134
+ def initialize(key)
135
+ @key = key
136
+ end
137
+
138
+ def errors
139
+ [JSONAPI::Error.new(code: JSONAPI::KEY_NOT_INCLUDED_IN_URL,
140
+ status: :bad_request,
141
+ title: 'Key is not included in URL',
142
+ detail: "The URL does not support the key #{key}")]
143
+ end
144
+ end
145
+
146
+ class MissingKey < Error
147
+ def errors
148
+ [JSONAPI::Error.new(code: JSONAPI::KEY_ORDER_MISMATCH,
149
+ status: :bad_request,
150
+ title: 'A key is required',
151
+ detail: 'The resource object does not contain a key.')]
152
+ end
153
+ end
154
+
155
+ class RecordLocked < Error
156
+ attr_accessor :message
157
+ def initialize(message)
158
+ @message = message
159
+ end
160
+
161
+ def errors
162
+ [JSONAPI::Error.new(code: JSONAPI::LOCKED,
163
+ status: :locked,
164
+ title: 'Locked resource',
165
+ detail: "#{message}")]
166
+ end
167
+ end
168
+
169
+ class ValidationErrors < Error
170
+ attr_accessor :errors
171
+ def initialize(errors)
172
+ @errors = errors
173
+ end
174
+ end
175
+
176
+ end
177
+ end
@@ -0,0 +1,151 @@
1
+ module JSONAPI
2
+ class Operation
3
+
4
+ attr_reader :resource_klass
5
+
6
+ def initialize(resource_klass)
7
+ @resource_klass = resource_klass
8
+ end
9
+
10
+ def apply(context)
11
+ end
12
+ end
13
+
14
+ class CreateResourceOperation < Operation
15
+ attr_reader :values
16
+
17
+ def initialize(resource_klass, values = {})
18
+ @values = values
19
+ super(resource_klass)
20
+ end
21
+
22
+ def apply(context)
23
+ resource = @resource_klass.new
24
+ resource.replace_fields(@values, context)
25
+ resource.save
26
+
27
+ return JSONAPI::OperationResult.new(:created, resource)
28
+
29
+ rescue JSONAPI::Exceptions::Error => e
30
+ return JSONAPI::OperationResult.new(e.errors.count == 1 ? e.errors[0].code : :bad_request, nil, e.errors)
31
+ end
32
+ end
33
+
34
+ class RemoveResourceOperation < Operation
35
+ attr_reader :resource_id
36
+ def initialize(resource_klass, resource_id)
37
+ @resource_id = resource_id
38
+ super(resource_klass)
39
+ end
40
+
41
+ def apply(context)
42
+ resource = @resource_klass.find_by_key(@resource_id, context)
43
+ resource.remove(context)
44
+
45
+ return JSONAPI::OperationResult.new(:no_content)
46
+
47
+ rescue ActiveRecord::DeleteRestrictionError => e
48
+ record_locked_error = JSONAPI::Exceptions::RecordLocked.new(e.message)
49
+ return JSONAPI::OperationResult.new(record_locked_error.errors[0].code, nil, record_locked_error.errors)
50
+ rescue JSONAPI::Exceptions::Error => e
51
+ return JSONAPI::OperationResult.new(e.errors.count == 1 ? e.errors[0].code : :bad_request, nil, e.errors)
52
+ end
53
+ end
54
+
55
+ class ReplaceFieldsOperation < Operation
56
+ attr_reader :values, :resource_id
57
+
58
+ def initialize(resource_klass, resource_id, values)
59
+ @resource_id = resource_id
60
+ @values = values
61
+ super(resource_klass)
62
+ end
63
+
64
+ def apply(context)
65
+ resource = @resource_klass.find_by_key(@resource_id, context)
66
+ resource.replace_fields(values, context)
67
+ resource.save
68
+
69
+ return JSONAPI::OperationResult.new(:ok, resource)
70
+
71
+ rescue JSONAPI::Exceptions::Error => e
72
+ return JSONAPI::OperationResult.new(e.errors.count == 1 ? e.errors[0].code : :bad_request, nil, e.errors)
73
+ end
74
+ end
75
+
76
+ class ReplaceHasOneAssociationOperation < Operation
77
+ attr_reader :resource_id, :association_type, :key_value
78
+
79
+ def initialize(resource_klass, resource_id, association_type, key_value)
80
+ @resource_id = resource_id
81
+ @key_value = key_value
82
+ @association_type = association_type
83
+ super(resource_klass)
84
+ end
85
+
86
+ def apply(context)
87
+ resource = @resource_klass.find_by_key(@resource_id, context)
88
+ resource.replace_has_one_link(@association_type, @key_value, context)
89
+ resource.save
90
+
91
+ return JSONAPI::OperationResult.new(:created, resource)
92
+ end
93
+ end
94
+
95
+ class CreateHasManyAssociationOperation < Operation
96
+ attr_reader :resource_id, :association_type, :key_values
97
+
98
+ def initialize(resource_klass, resource_id, association_type, key_values)
99
+ @resource_id = resource_id
100
+ @key_values = key_values
101
+ @association_type = association_type
102
+ super(resource_klass)
103
+ end
104
+
105
+ def apply(context)
106
+ resource = @resource_klass.find_by_key(@resource_id, context)
107
+ @key_values.each do |value|
108
+ resource.create_has_many_link(@association_type, value, context)
109
+ end
110
+
111
+ return JSONAPI::OperationResult.new(:created, resource)
112
+ end
113
+ end
114
+
115
+ class RemoveHasManyAssociationOperation < Operation
116
+ attr_reader :resource_id, :association_type, :associated_key
117
+
118
+ def initialize(resource_klass, resource_id, association_type, associated_key)
119
+ @resource_id = resource_id
120
+ @associated_key = associated_key
121
+ @association_type = association_type
122
+ super(resource_klass)
123
+ end
124
+
125
+ def apply(context)
126
+ resource = @resource_klass.find_by_key(@resource_id, context)
127
+ resource.remove_has_many_link(@association_type, @associated_key, context)
128
+
129
+ return JSONAPI::OperationResult.new(:no_content)
130
+ end
131
+
132
+ end
133
+
134
+ class RemoveHasOneAssociationOperation < Operation
135
+ attr_reader :resource_id, :association_type
136
+
137
+ def initialize(resource_klass, resource_id, association_type)
138
+ @resource_id = resource_id
139
+ @association_type = association_type
140
+ super(resource_klass)
141
+ end
142
+
143
+ def apply(context)
144
+ resource = @resource_klass.find_by_key(@resource_id, context)
145
+ resource.remove_has_one_link(@association_type, context)
146
+ resource.save
147
+
148
+ return JSONAPI::OperationResult.new(:no_content)
149
+ end
150
+ end
151
+ end