filemaker 0.0.19 → 1.0.0

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