jsonapi-resources 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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