fmrest-spyke 0.13.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 +7 -0
- data/.yardopts +4 -0
- data/CHANGELOG.md +127 -0
- data/LICENSE.txt +21 -0
- data/README.md +523 -0
- data/lib/fmrest-spyke.rb +3 -0
- data/lib/fmrest/spyke.rb +21 -0
- data/lib/fmrest/spyke/base.rb +9 -0
- data/lib/fmrest/spyke/container_field.rb +59 -0
- data/lib/fmrest/spyke/model.rb +33 -0
- data/lib/fmrest/spyke/model/associations.rb +166 -0
- data/lib/fmrest/spyke/model/attributes.rb +159 -0
- data/lib/fmrest/spyke/model/auth.rb +43 -0
- data/lib/fmrest/spyke/model/connection.rb +163 -0
- data/lib/fmrest/spyke/model/container_fields.rb +40 -0
- data/lib/fmrest/spyke/model/global_fields.rb +40 -0
- data/lib/fmrest/spyke/model/http.rb +77 -0
- data/lib/fmrest/spyke/model/orm.rb +256 -0
- data/lib/fmrest/spyke/model/record_id.rb +96 -0
- data/lib/fmrest/spyke/model/serialization.rb +115 -0
- data/lib/fmrest/spyke/model/uri.rb +29 -0
- data/lib/fmrest/spyke/portal.rb +65 -0
- data/lib/fmrest/spyke/relation.rb +359 -0
- data/lib/fmrest/spyke/spyke_formatter.rb +274 -0
- data/lib/fmrest/spyke/validation_error.rb +25 -0
- metadata +96 -0
data/lib/fmrest-spyke.rb
ADDED
data/lib/fmrest/spyke.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
begin
|
4
|
+
require "spyke"
|
5
|
+
rescue LoadError => e
|
6
|
+
e.message << " (Did you include Spyke in your Gemfile?)" unless e.message.frozen?
|
7
|
+
raise e
|
8
|
+
end
|
9
|
+
|
10
|
+
require "fmrest"
|
11
|
+
require "fmrest/spyke/spyke_formatter"
|
12
|
+
require "fmrest/spyke/model"
|
13
|
+
require "fmrest/spyke/base"
|
14
|
+
|
15
|
+
module FmRest
|
16
|
+
module Spyke
|
17
|
+
def self.included(base)
|
18
|
+
base.include Model
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module FmRest
|
4
|
+
module Spyke
|
5
|
+
class ContainerField
|
6
|
+
|
7
|
+
# @return [String] the name of the container field
|
8
|
+
attr_reader :name
|
9
|
+
|
10
|
+
# @param base [FmRest::Spyke::Base] the record this container belongs to
|
11
|
+
# @param name [Symbol] the name of the container field
|
12
|
+
def initialize(base, name)
|
13
|
+
@base = base
|
14
|
+
@name = name
|
15
|
+
end
|
16
|
+
|
17
|
+
# @return [String] the URL for the container
|
18
|
+
def url
|
19
|
+
@base.attributes[name]
|
20
|
+
end
|
21
|
+
|
22
|
+
# @return (see FmRest::V1::ContainerFields#fetch_container_data)
|
23
|
+
def download
|
24
|
+
FmRest::V1.fetch_container_data(url, @base.class.connection)
|
25
|
+
end
|
26
|
+
|
27
|
+
# @param filename_or_io [String, IO] a path to the file to upload or an
|
28
|
+
# IO object
|
29
|
+
# @param options [Hash]
|
30
|
+
# @option options [Integer] :repetition (1) The repetition to pass to the
|
31
|
+
# upload URL
|
32
|
+
# @option (see FmRest::V1::ContainerFields#upload_container_data)
|
33
|
+
def upload(filename_or_io, options = {})
|
34
|
+
raise ArgumentError, "Record needs to be saved before uploading to a container field" unless @base.persisted?
|
35
|
+
|
36
|
+
response =
|
37
|
+
FmRest::V1.upload_container_data(
|
38
|
+
@base.class.connection,
|
39
|
+
upload_path(options[:repetition] || 1),
|
40
|
+
filename_or_io,
|
41
|
+
options
|
42
|
+
)
|
43
|
+
|
44
|
+
# Update mod id on record
|
45
|
+
@base.__mod_id = response.body[:data][:__mod_id]
|
46
|
+
|
47
|
+
true
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
# @param repetition [Integer]
|
53
|
+
# @return [String] the path for uploading a file to the container
|
54
|
+
def upload_path(repetition)
|
55
|
+
FmRest::V1.container_field_path(@base.class.layout, @base.__record_id, name, repetition)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "fmrest/spyke/model/connection"
|
4
|
+
require "fmrest/spyke/model/uri"
|
5
|
+
require "fmrest/spyke/model/record_id"
|
6
|
+
require "fmrest/spyke/model/attributes"
|
7
|
+
require "fmrest/spyke/model/serialization"
|
8
|
+
require "fmrest/spyke/model/associations"
|
9
|
+
require "fmrest/spyke/model/orm"
|
10
|
+
require "fmrest/spyke/model/container_fields"
|
11
|
+
require "fmrest/spyke/model/global_fields"
|
12
|
+
require "fmrest/spyke/model/http"
|
13
|
+
require "fmrest/spyke/model/auth"
|
14
|
+
|
15
|
+
module FmRest
|
16
|
+
module Spyke
|
17
|
+
module Model
|
18
|
+
extend ::ActiveSupport::Concern
|
19
|
+
|
20
|
+
include Connection
|
21
|
+
include URI
|
22
|
+
include RecordID
|
23
|
+
include Attributes
|
24
|
+
include Serialization
|
25
|
+
include Associations
|
26
|
+
include Orm
|
27
|
+
include ContainerFields
|
28
|
+
include GlobalFields
|
29
|
+
include Http
|
30
|
+
include Auth
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,166 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "fmrest/spyke/portal"
|
4
|
+
|
5
|
+
module FmRest
|
6
|
+
module Spyke
|
7
|
+
module Model
|
8
|
+
# This module adds portal support to Spyke models.
|
9
|
+
#
|
10
|
+
module Associations
|
11
|
+
extend ::ActiveSupport::Concern
|
12
|
+
|
13
|
+
included do
|
14
|
+
# Keep track of portal options by their FM keys as we could need it
|
15
|
+
# to parse the portalData JSON in SpykeFormatter
|
16
|
+
class_attribute :portal_options, instance_accessor: false, instance_predicate: false
|
17
|
+
|
18
|
+
# class_attribute supports a :default option since ActiveSupport 5.2,
|
19
|
+
# but we want to support previous versions too so we set the default
|
20
|
+
# manually instead
|
21
|
+
self.portal_options = {}.freeze
|
22
|
+
|
23
|
+
class << self; private :portal_options=; end
|
24
|
+
|
25
|
+
set_callback :save, :after, :remove_marked_for_destruction
|
26
|
+
end
|
27
|
+
|
28
|
+
class_methods do
|
29
|
+
# Based on Spyke's `has_many`, but creates a special Portal
|
30
|
+
# association instead.
|
31
|
+
#
|
32
|
+
# @option :portal_key [String] The key used for the portal in the FM
|
33
|
+
# Data JSON portalData
|
34
|
+
# @option :attribute_prefix [String] The prefix used for portal
|
35
|
+
# attributes in the FM Data JSON
|
36
|
+
#
|
37
|
+
# @example
|
38
|
+
# class Person < FmRest::Spyke::Base
|
39
|
+
# has_portal :jobs, portal_key: "JobsTable", attribute_prefix: "Job"
|
40
|
+
# end
|
41
|
+
#
|
42
|
+
def has_portal(name, options = {})
|
43
|
+
create_association(name, Portal, options)
|
44
|
+
|
45
|
+
# Store options for SpykeFormatter to use if needed
|
46
|
+
portal_key = options[:portal_key] || name
|
47
|
+
self.portal_options = portal_options.merge(portal_key.to_s => options.dup.merge(name: name.to_s)).freeze
|
48
|
+
|
49
|
+
define_method "#{name.to_s.singularize}_ids" do
|
50
|
+
association(name).map(&:id)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# Spyke override -- Keep a cache of loaded portals. Spyke's default
|
56
|
+
# behavior is to reload the association each time.
|
57
|
+
#
|
58
|
+
def association(name)
|
59
|
+
@loaded_portals ||= {}
|
60
|
+
|
61
|
+
if @loaded_portals.has_key?(name.to_sym)
|
62
|
+
return @loaded_portals[name.to_sym]
|
63
|
+
end
|
64
|
+
|
65
|
+
super.tap do |assoc|
|
66
|
+
next unless assoc.kind_of?(FmRest::Spyke::Portal)
|
67
|
+
@loaded_portals[name.to_sym] = assoc
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# Spyke override -- Add portals awareness
|
72
|
+
#
|
73
|
+
def reload(*_)
|
74
|
+
super.tap { @loaded_portals = nil }
|
75
|
+
end
|
76
|
+
|
77
|
+
# @return [Array<FmRest::Spyke::Portal>] A collection of portal
|
78
|
+
# relations for the record
|
79
|
+
#
|
80
|
+
def portals
|
81
|
+
self.class.associations.each_with_object([]) do |(key, _), portals|
|
82
|
+
candidate = association(key)
|
83
|
+
next unless candidate.kind_of?(FmRest::Spyke::Portal)
|
84
|
+
portals << candidate
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
# Signals that this record has been marked for being deleted next time
|
89
|
+
# its parent record is saved (e.g. in a portal association)
|
90
|
+
#
|
91
|
+
# This method is named after ActiveRecord's namesake
|
92
|
+
#
|
93
|
+
def mark_for_destruction
|
94
|
+
@marked_for_destruction = true
|
95
|
+
end
|
96
|
+
alias_method :mark_for_deletion, :mark_for_destruction
|
97
|
+
|
98
|
+
def marked_for_destruction?
|
99
|
+
!!@marked_for_destruction
|
100
|
+
end
|
101
|
+
alias_method :marked_for_deletion?, :marked_for_destruction?
|
102
|
+
|
103
|
+
# Signals that this record has been embedded in a portal so we can make
|
104
|
+
# sure to include it in the next update request
|
105
|
+
#
|
106
|
+
def embedded_in_portal
|
107
|
+
@embedded_in_portal = true
|
108
|
+
end
|
109
|
+
|
110
|
+
def embedded_in_portal?
|
111
|
+
!!@embedded_in_portal
|
112
|
+
end
|
113
|
+
|
114
|
+
# Override ActiveModel::Dirty's method to include clearing
|
115
|
+
# of `@embedded_in_portal` and `@marked_for_destruction`
|
116
|
+
#
|
117
|
+
def changes_applied
|
118
|
+
super
|
119
|
+
@embedded_in_portal = nil
|
120
|
+
@marked_for_destruction = nil
|
121
|
+
end
|
122
|
+
|
123
|
+
# Override ActiveModel::Dirty's method to include awareness
|
124
|
+
# of `@embedded_in_portal`
|
125
|
+
#
|
126
|
+
def changed?
|
127
|
+
super || embedded_in_portal?
|
128
|
+
end
|
129
|
+
|
130
|
+
# Takes care of updating the new portal record's recordIds and modIds.
|
131
|
+
#
|
132
|
+
# Called when saving a record with freshly added portal records, this
|
133
|
+
# method is not meant to be called manually.
|
134
|
+
#
|
135
|
+
# @param [Hash] data The hash containing newPortalData from the DAPI
|
136
|
+
# response
|
137
|
+
def __new_portal_record_info=(data)
|
138
|
+
data.each do |d|
|
139
|
+
table_name = d[:tableName]
|
140
|
+
|
141
|
+
portal_new_records =
|
142
|
+
portals.detect { |p| p.portal_key == table_name }.select { |r| !r.persisted? }
|
143
|
+
|
144
|
+
# The DAPI provides only one recordId for the entire portal in the
|
145
|
+
# newPortalRecordInfo object. This appears to be the recordId of
|
146
|
+
# the last portal record created, so we assume all portal records
|
147
|
+
# coming before it must have sequential recordIds up to the one we
|
148
|
+
# do have.
|
149
|
+
portal_new_records.reverse_each.with_index do |record, i|
|
150
|
+
record.__record_id = d[:recordId].to_i - i
|
151
|
+
|
152
|
+
# New records get a fresh modId
|
153
|
+
record.__mod_id = 0
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
private
|
159
|
+
|
160
|
+
def remove_marked_for_destruction
|
161
|
+
portals.each(&:_remove_marked_for_destruction)
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
@@ -0,0 +1,159 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "fmrest/spyke/model/orm"
|
4
|
+
|
5
|
+
module FmRest
|
6
|
+
module Spyke
|
7
|
+
module Model
|
8
|
+
# Extends Spyke models with support for mapped attributes,
|
9
|
+
# `ActiveModel::Dirty` and forbidden attributes (e.g. Rails'
|
10
|
+
# `params.permit`).
|
11
|
+
#
|
12
|
+
module Attributes
|
13
|
+
extend ::ActiveSupport::Concern
|
14
|
+
|
15
|
+
include Orm # Needed to extend custom save and reload
|
16
|
+
|
17
|
+
include ::ActiveModel::Dirty
|
18
|
+
include ::ActiveModel::ForbiddenAttributesProtection
|
19
|
+
|
20
|
+
included do
|
21
|
+
# Prevent the creation of plain (no prefix/suffix) attribute methods
|
22
|
+
# when calling ActiveModels' define_attribute_method, otherwise it
|
23
|
+
# will define an `attribute` method which overrides the one provided
|
24
|
+
# by Spyke
|
25
|
+
self.attribute_method_matchers.shift
|
26
|
+
|
27
|
+
# Keep track of attribute mappings so we can get the FM field names
|
28
|
+
# for changed attributes
|
29
|
+
class_attribute :mapped_attributes, instance_writer: false, instance_predicate: false
|
30
|
+
|
31
|
+
# class_attribute supports a :default option since ActiveSupport 5.2,
|
32
|
+
# but we want to support previous versions too so we set the default
|
33
|
+
# manually instead
|
34
|
+
self.mapped_attributes = ::ActiveSupport::HashWithIndifferentAccess.new.freeze
|
35
|
+
|
36
|
+
class << self; private :mapped_attributes=; end
|
37
|
+
|
38
|
+
set_callback :save, :after, :changes_applied_after_save
|
39
|
+
end
|
40
|
+
|
41
|
+
class_methods do
|
42
|
+
# Spyke override
|
43
|
+
#
|
44
|
+
# Similar to Spyke::Base.attributes, but allows defining attribute
|
45
|
+
# methods that map to FM attributes with different names.
|
46
|
+
#
|
47
|
+
# @example
|
48
|
+
#
|
49
|
+
# class Person < Spyke::Base
|
50
|
+
# include FmRest::Spyke::Model
|
51
|
+
#
|
52
|
+
# attributes first_name: "FstName", last_name: "LstName"
|
53
|
+
# end
|
54
|
+
#
|
55
|
+
# p = Person.new
|
56
|
+
# p.first_name = "Jojo"
|
57
|
+
# p.attributes # => { "FstName" => "Jojo" }
|
58
|
+
#
|
59
|
+
def attributes(*attrs)
|
60
|
+
if attrs.length == 1 && attrs.first.kind_of?(Hash)
|
61
|
+
attrs.first.each { |from, to| _fmrest_define_attribute(from, to) }
|
62
|
+
else
|
63
|
+
attrs.each { |attr| _fmrest_define_attribute(attr, attr) }
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
# Spyke override (private)
|
70
|
+
#
|
71
|
+
# Called whenever loading records from the HTTP API, so we can reset
|
72
|
+
# dirty info on freshly loaded records
|
73
|
+
#
|
74
|
+
# See: https://github.com/balvig/spyke/blob/master/lib/spyke/http.rb
|
75
|
+
#
|
76
|
+
def new_or_return(attributes_or_object, *_)
|
77
|
+
# In case of an existing Spyke object return it as is so that we
|
78
|
+
# don't accidentally remove dirty data from associations
|
79
|
+
return super if attributes_or_object.is_a?(::Spyke::Base)
|
80
|
+
super.tap do |record|
|
81
|
+
# In ActiveModel 4.x #clear_changes_information is a private
|
82
|
+
# method, so we need to call it with send() in that case, but
|
83
|
+
# keep calling it normally for AM5+
|
84
|
+
if record.respond_to?(:clear_changes_information)
|
85
|
+
record.clear_changes_information
|
86
|
+
else
|
87
|
+
record.send(:clear_changes_information)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def _fmrest_attribute_methods_container
|
93
|
+
@fmrest_attribute_methods_container ||= Module.new.tap { |mod| include mod }
|
94
|
+
end
|
95
|
+
|
96
|
+
def _fmrest_define_attribute(from, to)
|
97
|
+
# We use a setter here instead of injecting the hash key/value pair
|
98
|
+
# directly with #[]= so that we don't change the mapped_attributes
|
99
|
+
# hash on the parent class. The resulting hash is frozen for the
|
100
|
+
# same reason.
|
101
|
+
self.mapped_attributes = mapped_attributes.merge(from => to).freeze
|
102
|
+
|
103
|
+
_fmrest_attribute_methods_container.module_eval do
|
104
|
+
define_method(from) do
|
105
|
+
attribute(to)
|
106
|
+
end
|
107
|
+
|
108
|
+
define_method(:"#{from}=") do |value|
|
109
|
+
send("#{from}_will_change!") unless value == send(from)
|
110
|
+
set_attribute(to, value)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
# Define ActiveModel::Dirty's methods
|
115
|
+
define_attribute_method(from)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
# Spyke override -- Adds AM::Dirty support
|
120
|
+
#
|
121
|
+
def reload(*args)
|
122
|
+
super.tap { |r| clear_changes_information }
|
123
|
+
end
|
124
|
+
|
125
|
+
# Spyke override -- Adds support for forbidden attributes (i.e. Rails'
|
126
|
+
# `params.permit`, etc.)
|
127
|
+
#
|
128
|
+
def attributes=(new_attributes)
|
129
|
+
@spyke_attributes ||= ::Spyke::Attributes.new(scope.params)
|
130
|
+
return unless new_attributes && !new_attributes.empty?
|
131
|
+
use_setters(sanitize_for_mass_assignment(new_attributes))
|
132
|
+
end
|
133
|
+
|
134
|
+
private
|
135
|
+
|
136
|
+
def changed_params
|
137
|
+
attributes.to_params.slice(*mapped_changed)
|
138
|
+
end
|
139
|
+
|
140
|
+
def mapped_changed
|
141
|
+
mapped_attributes.values_at(*changed)
|
142
|
+
end
|
143
|
+
|
144
|
+
# Spyke override (private) -- Use known mapped_attributes for inspect
|
145
|
+
#
|
146
|
+
def inspect_attributes
|
147
|
+
mapped_attributes.except(primary_key).map do |k, v|
|
148
|
+
"#{k}: #{attribute(v).inspect}"
|
149
|
+
end.join(', ')
|
150
|
+
end
|
151
|
+
|
152
|
+
def changes_applied_after_save
|
153
|
+
changes_applied
|
154
|
+
portals.each(&:parent_changes_applied)
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|