filemaker 0.0.19 → 1.0.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.
Files changed (48) hide show
  1. checksums.yaml +5 -5
  2. data/.editorconfig +14 -0
  3. data/.travis.yml +1 -2
  4. data/README.md +24 -10
  5. data/filemaker.gemspec +1 -4
  6. data/lib/filemaker.rb +14 -16
  7. data/lib/filemaker/configuration.rb +6 -1
  8. data/lib/filemaker/errors.rb +1 -0
  9. data/lib/filemaker/layout.rb +1 -2
  10. data/lib/filemaker/metadata/field.rb +6 -4
  11. data/lib/filemaker/model.rb +28 -17
  12. data/lib/filemaker/model/batches.rb +12 -1
  13. data/lib/filemaker/model/builder.rb +9 -4
  14. data/lib/filemaker/model/components.rb +8 -0
  15. data/lib/filemaker/model/field.rb +35 -40
  16. data/lib/filemaker/model/fields.rb +25 -22
  17. data/lib/filemaker/model/pagination.rb +1 -0
  18. data/lib/filemaker/model/persistable.rb +8 -8
  19. data/lib/filemaker/model/selectable.rb +1 -1
  20. data/lib/filemaker/model/type.rb +31 -0
  21. data/lib/filemaker/model/types/big_decimal.rb +22 -0
  22. data/lib/filemaker/model/types/date.rb +24 -0
  23. data/lib/filemaker/model/types/email.rb +5 -23
  24. data/lib/filemaker/model/types/integer.rb +22 -0
  25. data/lib/filemaker/model/types/text.rb +19 -0
  26. data/lib/filemaker/model/types/time.rb +22 -0
  27. data/lib/filemaker/record.rb +1 -1
  28. data/lib/filemaker/server.rb +38 -36
  29. data/lib/filemaker/store/database_store.rb +1 -1
  30. data/lib/filemaker/store/layout_store.rb +1 -1
  31. data/lib/filemaker/store/script_store.rb +1 -1
  32. data/lib/filemaker/version.rb +1 -1
  33. data/spec/filemaker/layout_spec.rb +1 -1
  34. data/spec/filemaker/metadata/field_spec.rb +16 -16
  35. data/spec/filemaker/model/builder_spec.rb +5 -0
  36. data/spec/filemaker/model/criteria_spec.rb +8 -7
  37. data/spec/filemaker/model/types_spec.rb +103 -0
  38. data/spec/filemaker/model_spec.rb +4 -13
  39. data/spec/filemaker/record_spec.rb +1 -1
  40. data/spec/filemaker/server_spec.rb +7 -9
  41. data/spec/filemaker/store/database_store_spec.rb +1 -1
  42. data/spec/filemaker/store/layout_store_spec.rb +1 -1
  43. data/spec/filemaker/store/script_store_spec.rb +1 -1
  44. data/spec/spec_helper.rb +5 -0
  45. data/spec/support/models.rb +0 -1
  46. data/spec/support/xml_loader.rb +8 -20
  47. metadata +14 -48
  48. data/lib/filemaker/model/types/attachment.rb +0 -102
@@ -19,18 +19,6 @@ module Filemaker
19
19
  module Fields
20
20
  extend ActiveSupport::Concern
21
21
 
22
- TYPE_MAPPINGS = {
23
- string: String,
24
- text: String,
25
- date: Date,
26
- datetime: DateTime,
27
- money: BigDecimal,
28
- number: BigDecimal,
29
- integer: Integer,
30
- email: Filemaker::Model::Types::Email,
31
- object: Filemaker::Model::Types::Attachment
32
- }.freeze
33
-
34
22
  included do
35
23
  class_attribute :fields, :identity
36
24
  self.fields = {}
@@ -41,7 +29,7 @@ module Filemaker
41
29
  def apply_defaults
42
30
  attribute_names.each do |name|
43
31
  field = fields[name]
44
- attributes[name] = field.default_value
32
+ instance_variable_set("@#{name}", field.default_value)
45
33
  end
46
34
  end
47
35
 
@@ -53,50 +41,65 @@ module Filemaker
53
41
  fields.values.map(&:fm_name)
54
42
  end
55
43
 
44
+ def attributes
45
+ fields.keys.each_with_object({}) do |field, hash|
46
+ # Attributes must be strings, not symbols - See
47
+ # http://api.rubyonrails.org/classes/ActiveModel/Serialization.html
48
+ hash[field.to_s] = instance_variable_get("@#{field}")
49
+
50
+ # If we use public_send(field) will encounter Stack Too Deep
51
+ end
52
+ end
53
+
56
54
  module ClassMethods
57
55
  def attribute_names
58
56
  fields.keys
59
57
  end
60
58
 
61
- TYPE_MAPPINGS.each_key do |type|
59
+ Filemaker::Model::Type.registry.each_key do |type|
62
60
  define_method(type) do |*args|
63
- # TODO: It will be good if we can accept lambda also
64
61
  options = args.last.is_a?(Hash) ? args.pop : {}
65
62
  field_names = args
66
63
 
67
64
  field_names.each do |name|
68
- add_field(name, TYPE_MAPPINGS[type.to_sym], options)
65
+ add_field(name, Filemaker::Model::Type.registry[type], options)
69
66
  create_accessors(name)
70
67
  end
71
68
  end
72
69
  end
73
70
 
74
71
  def add_field(name, type, options)
75
- name = name.to_s
72
+ name = name.to_s.freeze
76
73
  fields[name] = Filemaker::Model::Field.new(name, type, options)
77
74
  self.identity = fields[name] if options[:identity]
78
75
  end
79
76
 
80
77
  def create_accessors(name)
81
- name = name.to_s # Normalize it so ActiveModel::Serialization can work
78
+ # Normalize it so ActiveModel::Serialization can work
79
+ name = name.to_s
82
80
 
83
81
  define_attribute_methods name
84
82
 
85
83
  # Reader
86
84
  define_method(name) do
87
- attributes[name]
85
+ instance_variable_get("@#{name}")
88
86
  end
89
87
 
90
88
  # Writer - We try to map to the correct type, if not we just return
91
89
  # original.
92
90
  define_method("#{name}=") do |value|
93
- public_send("#{name}_will_change!") unless value == attributes[name]
94
- attributes[name] = fields[name].coerce(value, self.class)
91
+ new_value = fields[name].serialize_for_update(value)
92
+
93
+ public_send("#{name}_will_change!") \
94
+ if new_value != public_send(name)
95
+
96
+ instance_variable_set("@#{name}", new_value)
95
97
  end
96
98
 
97
99
  # Predicate
98
100
  define_method("#{name}?") do
99
- attributes[name] == true || attributes[name].present?
101
+ # See ActiveRecord::AttributeMethods::Query implementation
102
+ public_send(name) == true || public_send(name).present?
100
103
  end
101
104
  end
102
105
 
@@ -7,6 +7,7 @@ module Filemaker
7
7
  chains << :page
8
8
  @_page = positive_page(value.to_i)
9
9
  update_skip
10
+ all
10
11
  end
11
12
 
12
13
  def per(value)
@@ -28,6 +28,7 @@ module Filemaker
28
28
  options = {}
29
29
  yield options if block_given?
30
30
  resultset = api.new(fm_attributes, options)
31
+ changes_applied
31
32
  replace_new_data(resultset)
32
33
  end
33
34
  self
@@ -71,18 +72,17 @@ module Filemaker
71
72
 
72
73
  # If value is nil, we convert to empty string so it will get pick up by
73
74
  # `fm_attributes`
74
- def assign_attributes(new_attributes)
75
- return if new_attributes.blank?
75
+ # def assign_attributes(new_attributes)
76
+ # return if new_attributes.blank?
76
77
 
77
- new_attributes.each_pair do |key, value|
78
- next unless respond_to?("#{key}=")
78
+ # new_attributes.each_pair do |key, value|
79
+ # next unless respond_to?("#{key}=")
79
80
 
80
- public_send("#{key}=", (value || ''))
81
- end
82
- end
81
+ # public_send("#{key}=", (value || ''))
82
+ # end
83
+ # end
83
84
 
84
85
  def reload!
85
- reset_changes
86
86
  resultset = api.find(record_id)
87
87
  replace_new_data(resultset)
88
88
  self
@@ -69,7 +69,7 @@ module Filemaker
69
69
  @selector ||= {}
70
70
 
71
71
  criterion = if operator == 'bw'
72
- klass.with_model_fields(criterion, false)
72
+ klass.with_model_fields(criterion, use_query: false)
73
73
  else
74
74
  klass.with_model_fields(criterion)
75
75
  end
@@ -0,0 +1,31 @@
1
+ require 'filemaker/model/types/text'
2
+ require 'filemaker/model/types/date'
3
+ require 'filemaker/model/types/time'
4
+ require 'filemaker/model/types/big_decimal'
5
+ require 'filemaker/model/types/integer'
6
+ require 'filemaker/model/types/email'
7
+
8
+ module Filemaker
9
+ module Model
10
+ module Type
11
+ @registry = {}
12
+
13
+ class << self
14
+ attr_accessor :registry
15
+
16
+ def register(type_name, klass)
17
+ registry[type_name] = klass
18
+ end
19
+ end
20
+
21
+ register(:string, Filemaker::Model::Types::Text)
22
+ register(:text, Filemaker::Model::Types::Text)
23
+ register(:date, Filemaker::Model::Types::Date)
24
+ register(:datetime, Filemaker::Model::Types::Time)
25
+ register(:money, Filemaker::Model::Types::BigDecimal)
26
+ register(:number, Filemaker::Model::Types::BigDecimal)
27
+ register(:integer, Filemaker::Model::Types::Integer)
28
+ register(:email, Filemaker::Model::Types::Email)
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,22 @@
1
+ module Filemaker
2
+ module Model
3
+ module Types
4
+ class BigDecimal
5
+ def self.__filemaker_cast_to_ruby_object(value)
6
+ return value if value.is_a?(::BigDecimal)
7
+ BigDecimal(value.to_s)
8
+ end
9
+
10
+ def self.__filemaker_serialize_for_update(value)
11
+ return value if value.is_a?(::BigDecimal)
12
+ BigDecimal(value.to_s)
13
+ end
14
+
15
+ def self.__filemaker_serialize_for_query(value)
16
+ return value if value.is_a?(::BigDecimal)
17
+ BigDecimal(value.to_s)
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,24 @@
1
+ module Filemaker
2
+ module Model
3
+ module Types
4
+ class Date
5
+ def self.__filemaker_cast_to_ruby_object(value)
6
+ return value if value.is_a?(::Date)
7
+ ::Date.parse(value.to_s)
8
+ end
9
+
10
+ def self.__filemaker_serialize_for_update(value)
11
+ return value if value.is_a?(::Date)
12
+ ::Date.parse(value.to_s)
13
+ end
14
+
15
+ def self.__filemaker_serialize_for_query(value)
16
+ # If we are doing date range query like
17
+ # Model.where(date: '12/2018')
18
+ return value if value.is_a?(::Date) || value.is_a?(String)
19
+ ::Date.parse(value.to_s)
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -2,37 +2,19 @@ module Filemaker
2
2
  module Model
3
3
  module Types
4
4
  class Email
5
- def initialize(value)
6
- # to_s incoming value, this can prevent similar type from
7
- # nesting deeply
8
- @value = value.nil? ? nil : value.to_s
9
- end
10
-
11
- def value
12
- email = @value&.strip&.split(%r{,|\(|\/|\s})
5
+ def self.__filemaker_cast_to_ruby_object(value)
6
+ email = value&.strip&.split(%r{,|\(|\/|\s})
13
7
  &.reject(&:empty?)&.first&.downcase
14
8
  &.gsub(/[\uFF20\uFE6B\u0040]/, '@')
15
9
 
16
10
  email&.include?('@') ? email : nil
17
11
  end
18
12
 
19
- def value=(v)
20
- self.value = v
21
- end
22
-
23
- def to_s
24
- value
25
- end
26
-
27
- def ==(other)
28
- to_s == other.to_s
13
+ def self.__filemaker_serialize_for_update(value)
14
+ __filemaker_cast_to_ruby_object(value)
29
15
  end
30
- alias eql? ==
31
16
 
32
- # In FileMaker, at-sign is for wildcard query. In order to search for
33
- # email, we need to escape at-sign. Note the single-quote escaping!
34
- # e.g. 'a@host.com' will become 'a\\@host.com'
35
- def to_query
17
+ def self.__filemaker_serialize_for_query(value)
36
18
  value.gsub('@', '\@')
37
19
  end
38
20
  end
@@ -0,0 +1,22 @@
1
+ module Filemaker
2
+ module Model
3
+ module Types
4
+ class Integer
5
+ def self.__filemaker_cast_to_ruby_object(value)
6
+ return value if value.is_a?(::Integer)
7
+ value.to_i
8
+ end
9
+
10
+ def self.__filemaker_serialize_for_update(value)
11
+ return value if value.is_a?(::Integer)
12
+ value.to_i
13
+ end
14
+
15
+ def self.__filemaker_serialize_for_query(value)
16
+ return value if value.is_a?(::Integer)
17
+ value.to_i
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,19 @@
1
+ module Filemaker
2
+ module Model
3
+ module Types
4
+ class Text
5
+ def self.__filemaker_cast_to_ruby_object(value)
6
+ value.to_s
7
+ end
8
+
9
+ def self.__filemaker_serialize_for_update(value)
10
+ value.to_s
11
+ end
12
+
13
+ def self.__filemaker_serialize_for_query(value)
14
+ value.to_s
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,22 @@
1
+ module Filemaker
2
+ module Model
3
+ module Types
4
+ class Time
5
+ def self.__filemaker_cast_to_ruby_object(value)
6
+ return value if value.is_a?(::Time)
7
+ ::Time.parse(value.to_s)
8
+ end
9
+
10
+ def self.__filemaker_serialize_for_update(value)
11
+ return value if value.is_a?(::Time)
12
+ ::Time.parse(value.to_s)
13
+ end
14
+
15
+ def self.__filemaker_serialize_for_query(value)
16
+ return value if value.is_a?(::Time) || value.is_a?(String)
17
+ ::Time.parse(value.to_s)
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -33,7 +33,7 @@ module Filemaker
33
33
  end
34
34
 
35
35
  field.xpath('data').each do |data|
36
- datum.push(metadata_fields[field_name].coerce(data.inner_text))
36
+ datum.push(metadata_fields[field_name].raw_cast(data.inner_text))
37
37
  end
38
38
 
39
39
  self[field_name] = normalize_data(datum)
@@ -1,29 +1,25 @@
1
- require 'faraday'
2
- require 'typhoeus/adapters/faraday'
1
+ require 'typhoeus'
3
2
  require 'filemaker/configuration'
4
3
 
5
4
  module Filemaker
6
5
  class Server
7
6
  extend Forwardable
8
7
 
9
- # @return [Faraday::Connection] the HTTP connection
10
- attr_reader :connection
11
-
12
8
  # @return [Filemaker::Store::DatabaseStore] the database store
13
9
  attr_reader :databases
14
10
  alias database databases
15
11
  alias db databases
16
12
 
17
- def_delegators :@config, :host, :url, :ssl, :endpoint, :log
13
+ def_delegators :@config, :host, :url, :endpoint, :log
18
14
  def_delegators :@config, :account_name, :password
15
+ def_delegators :@config, :ssl_verifypeer, :ssl_verifyhost, :ssl, :timeout
19
16
 
20
- def initialize(options = {})
17
+ def initialize
21
18
  @config = Configuration.new
22
19
  yield @config if block_given?
23
20
  raise ArgumentError, 'Missing config block' if @config.not_configurable?
24
21
 
25
22
  @databases = Store::DatabaseStore.new(self)
26
- @connection = get_connection(options)
27
23
  end
28
24
 
29
25
  # @api private
@@ -33,8 +29,8 @@ module Filemaker
33
29
  # Also we want to pass in timeout option so we can ignore timeout for really
34
30
  # long requests
35
31
  #
36
- # @return [Array] Faraday::Response and request params Hash
37
- def perform_request(method, action, args, options = {})
32
+ # @return [Array] response and request params Hash
33
+ def perform_request(action, args, options = {})
38
34
  params = serialize_args(args)
39
35
  .merge(expand_options(options))
40
36
  .merge({ action => '' })
@@ -45,28 +41,36 @@ module Filemaker
45
41
  log_action(params)
46
42
 
47
43
  # yield params if block_given?
48
- response = @connection.public_send(method, endpoint, params)
49
44
 
50
- case response.status
45
+ response = get_typhoeus_connection(params)
46
+
47
+ http_status = "#{response.response_code}:#{response.return_code}"
48
+
49
+ case response.response_code
51
50
  when 200
52
51
  [response, params]
53
52
  when 401
54
53
  raise Errors::AuthenticationError,
55
- "[#{response.status}] Authentication failed."
54
+ "[#{http_status}] Authentication failed."
56
55
  when 0
57
- raise Errors::CommunicationError,
58
- "[#{response.status}] Empty response."
56
+ if response.return_code == :operation_timedout
57
+ raise Errors::HttpTimeoutError,
58
+ "[#{http_status}] Current timeout value is #{timeout}"
59
+ else
60
+ raise Errors::CommunicationError,
61
+ "[#{http_status}] Empty response."
62
+ end
59
63
  when 404
60
64
  raise Errors::CommunicationError,
61
- "[#{response.status}] Not found"
65
+ "[#{http_status}] Not found"
62
66
  when 302
63
67
  raise Errors::CommunicationError,
64
- "[#{response.status}] Redirect not supported"
68
+ "[#{http_status}] Redirect not supported"
65
69
  when 502
66
70
  raise Errors::CommunicationError,
67
- "[#{response.status}] Bad gateway. Too many records."
71
+ "[#{http_status}] Bad gateway. Too many records."
68
72
  else
69
- msg = "Unknown response status = #{response.status}"
73
+ msg = "Unknown response code = #{http_status}"
70
74
  raise Errors::CommunicationError, msg
71
75
  end
72
76
  end
@@ -77,21 +81,22 @@ module Filemaker
77
81
 
78
82
  private
79
83
 
80
- def get_connection(options = {})
81
- faraday_options = @config.connection_options.merge(options)
82
-
83
- Faraday.new(@config.url, faraday_options) do |faraday|
84
- faraday.request :url_encoded
85
- faraday.headers[:user_agent] = \
86
- "filemaker-ruby-#{Filemaker::VERSION}".freeze
87
- faraday.basic_auth @config.account_name, @config.password
88
-
89
- # The order of the middleware is important, so adapter must be the last
90
- faraday.adapter :typhoeus
91
- end
84
+ def get_typhoeus_connection(body)
85
+ request = Typhoeus::Request.new(
86
+ "#{url}#{endpoint}",
87
+ method: :post,
88
+ ssl_verifypeer: ssl_verifypeer,
89
+ ssl_verifyhost: ssl_verifyhost,
90
+ userpwd: "#{account_name}:#{password}",
91
+ body: body,
92
+ timeout: timeout || 0
93
+ )
94
+
95
+ request.run
92
96
  end
93
97
 
94
98
  # {"-db"=>"mydb", "-lay"=>"mylay", "email"=>"a@b.com", "updated_at": Date}
99
+ # Take Ruby type and serialize into a form FileMaker can understand
95
100
  def serialize_args(args)
96
101
  return {} if args.nil?
97
102
 
@@ -103,8 +108,6 @@ module Filemaker
103
108
  args[key] = value.strftime('%m/%d/%Y')
104
109
  when Time
105
110
  args[key] = value.strftime('%H:%M')
106
- when Filemaker::Model::Types::Email
107
- args[key] = value.to_query
108
111
  else
109
112
  # Especially for range operator (...), we want to output as String
110
113
  args[key] = value.to_s
@@ -196,10 +199,9 @@ module Filemaker
196
199
  curl_ssl_option = ''
197
200
  auth = ''
198
201
 
199
- curl_ssl_option = ' -k' if ssl.is_a?(Hash) && !ssl.fetch(:verify) { true }
202
+ curl_ssl_option = ' -k' unless ssl_verifypeer
200
203
 
201
- auth = " -H 'Authorization: #{@connection.headers['Authorization']}'" if \
202
- has_auth
204
+ auth = " -u #{account_name}:[FILTERED]" if has_auth
203
205
 
204
206
  # warn 'Pretty print like so: `curl XXX | xmllint --format -`'
205
207
  warn "curl -XGET '#{full_url}'#{curl_ssl_option} -i#{auth}"