fmrest 0.11.1 → 0.15.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +2 -0
  3. data/CHANGELOG.md +33 -0
  4. data/README.md +247 -847
  5. metadata +82 -101
  6. data/.github/workflows/ci.yml +0 -33
  7. data/.gitignore +0 -26
  8. data/.rspec +0 -3
  9. data/.travis.yml +0 -5
  10. data/Gemfile +0 -3
  11. data/Rakefile +0 -6
  12. data/fmrest.gemspec +0 -38
  13. data/lib/fmrest.rb +0 -36
  14. data/lib/fmrest/connection_settings.rb +0 -124
  15. data/lib/fmrest/errors.rb +0 -30
  16. data/lib/fmrest/spyke.rb +0 -21
  17. data/lib/fmrest/spyke/base.rb +0 -23
  18. data/lib/fmrest/spyke/container_field.rb +0 -59
  19. data/lib/fmrest/spyke/model.rb +0 -36
  20. data/lib/fmrest/spyke/model/associations.rb +0 -82
  21. data/lib/fmrest/spyke/model/attributes.rb +0 -171
  22. data/lib/fmrest/spyke/model/auth.rb +0 -43
  23. data/lib/fmrest/spyke/model/connection.rb +0 -135
  24. data/lib/fmrest/spyke/model/container_fields.rb +0 -25
  25. data/lib/fmrest/spyke/model/global_fields.rb +0 -40
  26. data/lib/fmrest/spyke/model/http.rb +0 -37
  27. data/lib/fmrest/spyke/model/orm.rb +0 -212
  28. data/lib/fmrest/spyke/model/serialization.rb +0 -99
  29. data/lib/fmrest/spyke/model/uri.rb +0 -30
  30. data/lib/fmrest/spyke/portal.rb +0 -55
  31. data/lib/fmrest/spyke/relation.rb +0 -359
  32. data/lib/fmrest/spyke/spyke_formatter.rb +0 -273
  33. data/lib/fmrest/spyke/validation_error.rb +0 -25
  34. data/lib/fmrest/string_date.rb +0 -220
  35. data/lib/fmrest/token_store.rb +0 -12
  36. data/lib/fmrest/token_store/active_record.rb +0 -74
  37. data/lib/fmrest/token_store/base.rb +0 -25
  38. data/lib/fmrest/token_store/memory.rb +0 -26
  39. data/lib/fmrest/token_store/moneta.rb +0 -41
  40. data/lib/fmrest/token_store/redis.rb +0 -45
  41. data/lib/fmrest/v1.rb +0 -23
  42. data/lib/fmrest/v1/auth.rb +0 -30
  43. data/lib/fmrest/v1/connection.rb +0 -115
  44. data/lib/fmrest/v1/container_fields.rb +0 -114
  45. data/lib/fmrest/v1/dates.rb +0 -81
  46. data/lib/fmrest/v1/paths.rb +0 -47
  47. data/lib/fmrest/v1/raise_errors.rb +0 -57
  48. data/lib/fmrest/v1/token_session.rb +0 -133
  49. data/lib/fmrest/v1/token_store/active_record.rb +0 -13
  50. data/lib/fmrest/v1/token_store/memory.rb +0 -13
  51. data/lib/fmrest/v1/type_coercer.rb +0 -192
  52. data/lib/fmrest/v1/utils.rb +0 -94
  53. data/lib/fmrest/version.rb +0 -5
@@ -1,124 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module FmRest
4
- # Wrapper class for connection settings hash, with a number of purposes:
5
- #
6
- # * Provide indifferent access (base hash can have either string or symbol
7
- # keys)
8
- # * Method access
9
- # * Default values
10
- # * Basic validation
11
- # * Normalization (e.g. aliased settings)
12
- # * Useful error messages
13
- class ConnectionSettings
14
- class MissingSetting < ArgumentError; end
15
-
16
- PROPERTIES = %i(
17
- host
18
- database
19
- username
20
- password
21
- token
22
- token_store
23
- autologin
24
- ssl
25
- proxy
26
- log
27
- coerce_dates
28
- date_format
29
- timestamp_format
30
- time_format
31
- timezone
32
- ).freeze
33
-
34
- # NOTE: password intentionally left non-required since it's only really
35
- # needed when no token exists, and should only be required when logging in
36
- REQUIRED = %i(
37
- host
38
- database
39
- ).freeze
40
-
41
- DEFAULT_DATE_FORMAT = "MM/dd/yyyy"
42
- DEFAULT_TIME_FORMAT = "HH:mm:ss"
43
- DEFAULT_TIMESTAMP_FORMAT = "#{DEFAULT_DATE_FORMAT} #{DEFAULT_TIME_FORMAT}"
44
-
45
- DEFAULTS = {
46
- autologin: true,
47
- log: false,
48
- date_format: DEFAULT_DATE_FORMAT,
49
- time_format: DEFAULT_TIME_FORMAT,
50
- timestamp_format: DEFAULT_TIMESTAMP_FORMAT,
51
- coerce_dates: false
52
- }.freeze
53
-
54
- def self.wrap(settings, skip_validation: false)
55
- if settings.kind_of?(self)
56
- settings.validate unless skip_validation
57
- return settings
58
- end
59
- new(settings, skip_validation: skip_validation)
60
- end
61
-
62
- def initialize(settings, skip_validation: false)
63
- @settings = settings.to_h.dup
64
- normalize
65
- validate unless skip_validation
66
- end
67
-
68
- PROPERTIES.each do |p|
69
- define_method(p) do
70
- get(p)
71
- end
72
-
73
- define_method("#{p}!") do
74
- r = get(p)
75
- raise MissingSetting, "Missing required setting: `#{p}'" if r.nil?
76
- r
77
- end
78
-
79
- define_method("#{p}?") do
80
- !!get(p)
81
- end
82
- end
83
-
84
- def [](key)
85
- raise ArgumentError, "Unknown setting `#{key}'" unless PROPERTIES.include?(key.to_sym)
86
- get(key)
87
- end
88
-
89
- def to_h
90
- PROPERTIES.each_with_object({}) do |p, h|
91
- v = get(p)
92
- h[p] = v unless v == DEFAULTS[p]
93
- end
94
- end
95
-
96
- def merge(other, **keyword_args)
97
- other = self.class.wrap(other, skip_validation: true)
98
- self.class.new(to_h.merge(other.to_h), **keyword_args)
99
- end
100
-
101
- def validate
102
- missing = REQUIRED.select { |r| get(r).nil? }.map { |m| "`#{m}'" }
103
- raise MissingSetting, "Missing required setting(s): #{missing.join(', ')}" unless missing.empty?
104
-
105
- unless username? || token?
106
- raise MissingSetting, "A minimum of `username' or `token' are required to be able to establish a connection"
107
- end
108
- end
109
-
110
- private
111
-
112
- def get(key)
113
- return @settings[key.to_sym] if @settings.has_key?(key.to_sym)
114
- return @settings[key.to_s] if @settings.has_key?(key.to_s)
115
- DEFAULTS[key.to_sym]
116
- end
117
-
118
- def normalize
119
- if !get(:username) && account_name = get(:account_name)
120
- @settings[:username] = account_name
121
- end
122
- end
123
- end
124
- end
data/lib/fmrest/errors.rb DELETED
@@ -1,30 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module FmRest
4
- class Error < StandardError; end
5
-
6
- class APIError < Error
7
- attr_reader :code
8
-
9
- def initialize(code, message = nil)
10
- @code = code
11
- super("FileMaker Data API responded with error #{code}: #{message}")
12
- end
13
- end
14
-
15
- class APIError::UnknownError < APIError; end # error code -1
16
- class APIError::ResourceMissingError < APIError; end # error codes 100..199
17
- class APIError::RecordMissingError < APIError::ResourceMissingError; end
18
- class APIError::AccountError < APIError; end # error codes 200..299
19
- class APIError::LockError < APIError; end # error codes 300..399
20
- class APIError::ParameterError < APIError; end # error codes 400..499
21
- class APIError::NoMatchingRecordsError < APIError::ParameterError; end
22
- class APIError::ValidationError < APIError; end # error codes 500..599
23
- class APIError::SystemError < APIError; end # error codes 800..899
24
- class APIError::InvalidToken < APIError; end # error code 952
25
- class APIError::MaximumDataAPICallsExceeded < APIError; end # error code 953
26
- class APIError::ScriptError < APIError; end # error codes 1200..1299
27
- class APIError::ODBCError < APIError; end # error codes 1400..1499
28
-
29
- class ContainerFieldError < Error; end
30
- end
data/lib/fmrest/spyke.rb DELETED
@@ -1,21 +0,0 @@
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
@@ -1,23 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module FmRest
4
- module Spyke
5
- class Base < ::Spyke::Base
6
- include FmRest::Spyke::Model
7
- end
8
-
9
- class << self
10
- def Base(config = nil)
11
- warn "[DEPRECATION] Inheriting from `FmRest::Spyke::Base(config)` is deprecated and will be removed, inherit from `FmRest::Spyke::Base` (without arguments) and use `fmrest_config=` instead"
12
-
13
- if config
14
- return Class.new(::FmRest::Spyke::Base) do
15
- self.fmrest_config = config
16
- end
17
- end
18
-
19
- ::FmRest::Spyke::Base
20
- end
21
- end
22
- end
23
- end
@@ -1,59 +0,0 @@
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.id, name, repetition)
56
- end
57
- end
58
- end
59
- end
@@ -1,36 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "fmrest/spyke/model/connection"
4
- require "fmrest/spyke/model/uri"
5
- require "fmrest/spyke/model/attributes"
6
- require "fmrest/spyke/model/serialization"
7
- require "fmrest/spyke/model/associations"
8
- require "fmrest/spyke/model/orm"
9
- require "fmrest/spyke/model/container_fields"
10
- require "fmrest/spyke/model/global_fields"
11
- require "fmrest/spyke/model/http"
12
- require "fmrest/spyke/model/auth"
13
-
14
- module FmRest
15
- module Spyke
16
- module Model
17
- extend ::ActiveSupport::Concern
18
-
19
- include Connection
20
- include Uri
21
- include Attributes
22
- include Serialization
23
- include Associations
24
- include Orm
25
- include ContainerFields
26
- include GlobalFields
27
- include Http
28
- include Auth
29
-
30
- included do
31
- # @return [Integer] the record's modId
32
- attr_accessor :mod_id
33
- end
34
- end
35
- end
36
- end
@@ -1,82 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "fmrest/spyke/portal"
4
-
5
- module FmRest
6
- module Spyke
7
- module Model
8
- module Associations
9
- extend ::ActiveSupport::Concern
10
-
11
- included do
12
- # Keep track of portal options by their FM keys as we could need it
13
- # to parse the portalData JSON in SpykeFormatter
14
- class_attribute :portal_options, instance_accessor: false, instance_predicate: false
15
-
16
- # class_attribute supports a :default option since ActiveSupport 5.2,
17
- # but we want to support previous versions too so we set the default
18
- # manually instead
19
- self.portal_options = {}.freeze
20
-
21
- class << self; private :portal_options=; end
22
- end
23
-
24
- class_methods do
25
- # Based on +has_many+, but creates a special Portal association
26
- # instead.
27
- #
28
- # Custom options:
29
- #
30
- # * <tt>:portal_key</tt> - The key used for the portal in the FM Data JSON portalData.
31
- # * <tt>:attribute_prefix</tt> - The prefix used for portal attributes in the FM Data JSON.
32
- #
33
- # Example:
34
- #
35
- # has_portal :jobs, portal_key: "JobsTable", attribute_prefix: "Job"
36
- #
37
- def has_portal(name, options = {})
38
- create_association(name, Portal, options)
39
-
40
- # Store options for SpykeFormatter to use if needed
41
- portal_key = options[:portal_key] || name
42
- self.portal_options = portal_options.merge(portal_key.to_s => options.dup.merge(name: name.to_s)).freeze
43
-
44
- define_method "#{name.to_s.singularize}_ids" do
45
- association(name).map(&:id)
46
- end
47
- end
48
- end
49
-
50
- # Override Spyke's association reader to keep a cache of loaded
51
- # portals. Spyke's default behavior is to reload the association
52
- # each time.
53
- #
54
- def association(name)
55
- @loaded_portals ||= {}
56
-
57
- if @loaded_portals.has_key?(name.to_sym)
58
- return @loaded_portals[name.to_sym]
59
- end
60
-
61
- super.tap do |assoc|
62
- next unless assoc.kind_of?(FmRest::Spyke::Portal)
63
- @loaded_portals[name.to_sym] = assoc
64
- end
65
- end
66
-
67
- def reload(*_)
68
- super.tap { @loaded_portals = nil }
69
- end
70
-
71
- def portals
72
- self.class.associations.each_with_object([]) do |(key, _), portals|
73
- candidate = association(key)
74
- next unless candidate.kind_of?(FmRest::Spyke::Portal)
75
- portals << candidate
76
- end
77
- end
78
- end
79
- end
80
- end
81
- end
82
-
@@ -1,171 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "fmrest/spyke/model/orm"
4
-
5
- module FmRest
6
- module Spyke
7
- module Model
8
- module Attributes
9
- extend ::ActiveSupport::Concern
10
-
11
- include Orm # Needed to extend custom save and reload
12
-
13
- include ::ActiveModel::Dirty
14
- include ::ActiveModel::ForbiddenAttributesProtection
15
-
16
- included do
17
- # Prevent the creation of plain (no prefix/suffix) attribute methods
18
- # when calling ActiveModels' define_attribute_method, otherwise it
19
- # will define an `attribute` method which overrides the one provided
20
- # by Spyke
21
- self.attribute_method_matchers.shift
22
-
23
- # ActiveModel::Dirty methods for id
24
- define_attribute_method(:id)
25
-
26
- # Keep track of attribute mappings so we can get the FM field names
27
- # for changed attributes
28
- class_attribute :mapped_attributes, instance_writer: false, instance_predicate: false
29
-
30
- # class_attribute supports a :default option since ActiveSupport 5.2,
31
- # but we want to support previous versions too so we set the default
32
- # manually instead
33
- self.mapped_attributes = ::ActiveSupport::HashWithIndifferentAccess.new.freeze
34
-
35
- class << self; private :mapped_attributes=; end
36
- end
37
-
38
- class_methods do
39
- # Similar to Spyke::Base.attributes, but allows defining attribute
40
- # methods that map to FM attributes with different names.
41
- #
42
- # Example:
43
- #
44
- # class Person < Spyke::Base
45
- # include FmRest::Spyke::Model
46
- #
47
- # attributes first_name: "FstName", last_name: "LstName"
48
- # end
49
- #
50
- # p = Person.new
51
- # p.first_name = "Jojo"
52
- # p.attributes # => { "FstName" => "Jojo" }
53
- #
54
- def attributes(*attrs)
55
- if attrs.length == 1 && attrs.first.kind_of?(Hash)
56
- attrs.first.each { |from, to| _fmrest_define_attribute(from, to) }
57
- else
58
- attrs.each { |attr| _fmrest_define_attribute(attr, attr) }
59
- end
60
- end
61
-
62
- private
63
-
64
- # Override Spyke::Base.new_or_return (private), called whenever
65
- # loading records from the HTTP API, so we can reset dirty info on
66
- # freshly loaded records
67
- #
68
- # See: https://github.com/balvig/spyke/blob/master/lib/spyke/http.rb
69
- #
70
- def new_or_return(attributes_or_object, *_)
71
- # In case of an existing Spyke object return it as is so that we
72
- # don't accidentally remove dirty data from associations
73
- return super if attributes_or_object.is_a?(::Spyke::Base)
74
- super.tap do |record|
75
- # In ActiveModel 4.x #clear_changes_information is a private
76
- # method, so we need to call it with send() in that case, but
77
- # keep calling it normally for AM5+
78
- if record.respond_to?(:clear_changes_information)
79
- record.clear_changes_information
80
- else
81
- record.send(:clear_changes_information)
82
- end
83
- end
84
- end
85
-
86
- def _fmrest_attribute_methods_container
87
- @fmrest_attribute_methods_container ||= Module.new.tap { |mod| include mod }
88
- end
89
-
90
- def _fmrest_define_attribute(from, to)
91
- raise ArgumentError, "attribute name `id' is reserved for the recordId" if from.to_s == "id"
92
-
93
- # We use a setter here instead of injecting the hash key/value pair
94
- # directly with #[]= so that we don't change the mapped_attributes
95
- # hash on the parent class. The resulting hash is frozen for the
96
- # same reason.
97
- self.mapped_attributes = mapped_attributes.merge(from => to).freeze
98
-
99
- _fmrest_attribute_methods_container.module_eval do
100
- define_method(from) do
101
- attribute(to)
102
- end
103
-
104
- define_method(:"#{from}=") do |value|
105
- send("#{from}_will_change!") unless value == send(from)
106
- set_attribute(to, value)
107
- end
108
- end
109
-
110
- # Define ActiveModel::Dirty's methods
111
- define_attribute_method(from)
112
- end
113
- end
114
-
115
- def id=(value)
116
- id_will_change! unless value == id
117
- super
118
- end
119
-
120
- def reload(*args)
121
- super.tap { |r| clear_changes_information }
122
- end
123
-
124
- def save(*args)
125
- super.tap { |r| changes_applied_after_save if r }
126
- end
127
-
128
- # ActiveModel::Dirty since version 5.2 assumes that if there's an
129
- # @attributes instance variable set we must be using ActiveRecord, so
130
- # we override the instance variable name used by Spyke to avoid issues.
131
- #
132
- # TODO: Submit a pull request to Spyke so this isn't needed
133
- #
134
- def attributes
135
- @_spyke_attributes
136
- end
137
-
138
- # In addition to the comments above on `attributes`, this also adds
139
- # support for forbidden attributes
140
- #
141
- def attributes=(new_attributes)
142
- @_spyke_attributes ||= ::Spyke::Attributes.new(scope.params)
143
- use_setters(sanitize_for_mass_assignment(new_attributes)) if new_attributes && !new_attributes.empty?
144
- end
145
-
146
- private
147
-
148
- def changed_params
149
- attributes.to_params.slice(*mapped_changed)
150
- end
151
-
152
- def mapped_changed
153
- mapped_attributes.values_at(*changed)
154
- end
155
-
156
- # Use known mapped_attributes for inspect
157
- #
158
- def inspect_attributes
159
- mapped_attributes.except(primary_key).map do |k, v|
160
- "#{k}: #{attribute(v).inspect}"
161
- end.join(', ')
162
- end
163
-
164
- def changes_applied_after_save
165
- changes_applied
166
- portals.each(&:parent_changes_applied)
167
- end
168
- end
169
- end
170
- end
171
- end