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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 267ea79af38848d21b296940abe2e14d3ce53f0f
4
- data.tar.gz: 6cdd38cc8b20df9d9a2402635f124b178fd3e6e3
2
+ SHA256:
3
+ metadata.gz: 90e8d2b8704500b90fa6649ad585301c88f94e420bb34f4cd620a1c7c53abe9d
4
+ data.tar.gz: 65fff04efa068012d5fcc54a1877c50b15434dde10040ad41a146b3889267f77
5
5
  SHA512:
6
- metadata.gz: a055b4eca018e965d88265b5719571f1ad079838addf511cb57f625e2e32d9e9cb0f413cd904433d29cb62d52b3736413ee917e7563e14d5dfa9c6d97b6b3530
7
- data.tar.gz: 22e268a6b6ceb67adec4d167aefe9771be0d28e7a3728454578d3e8aedc659918605fdf84e724d6e0b8a2f9d0860de368b5bdb3f76ae9cabada1451094c58b5a
6
+ metadata.gz: a858783d95970eca0e7166f84e494a755885b86674c895574b3563fbfee44f4b2fc8c249a20580062c5c649c9bb9b6812230e7e787235ac5fcdaca396b111810
7
+ data.tar.gz: 4e5b6d9fc0d0ae3bf85ded1daee20c85e4fbac8d657fbfd4ce56231e8d5ac4a49837c8a41a966d2050d0455fbe302688c43c0181df4bd3bf4c700786bbd0f69f
@@ -0,0 +1,14 @@
1
+ # http://editorconfig.org
2
+ root = true
3
+
4
+ [*]
5
+ charset = utf-8
6
+ end_of_line = lf
7
+ indent_size = 2
8
+ indent_style = space
9
+ insert_final_newline = true
10
+ trim_trailing_whitespace = true
11
+
12
+ [*.md]
13
+ max_line_length = 0
14
+ trim_trailing_whitespace = false
@@ -1,5 +1,4 @@
1
1
  language: ruby
2
2
  rvm:
3
- - 2.3.0
4
- - 2.4.0
3
+ - 2.5.0
5
4
  script: "bundle exec rake"
data/README.md CHANGED
@@ -23,7 +23,9 @@ Configuration for initializing a server:
23
23
  * `host` - IP or hostname
24
24
  * `account` - Please use `ENV` variable like `ENV['FILEMAKER_ACCOUNT']`
25
25
  * `password` - Please use `ENV` variable like `ENV['FILEMAKER_PASSWORD']`
26
- * `ssl` - Use `{ verify: false }` if you are using FileMaker's unsigned certificate. You can also pass a hash which will be forwarded to Faraday directly like `ssl: { client_cert: '', client_key: '', ca_file: '', ca_path: '/path/to/certs', cert_store: '' }`. See [Setting up SSL certificates on the Faraday wiki](https://github.com/lostisland/faraday/wiki/Setting-up-SSL-certificates)
26
+ * `ssl` - Use `{ verify: false }` if you are using FileMaker's unsigned certificate.
27
+ * `ssl_verifypeer` - Default to `false`
28
+ * `ssl_verifyhost` - Default to `0`
27
29
  * `log` - A choice of `simple`, `curl` and `curl_auth`.
28
30
 
29
31
  ```ruby
@@ -69,15 +71,26 @@ If you want ActiveModel-like access with a decent query DSL like `where`, `find`
69
71
 
70
72
  The following data type mappings can be used to register the fields:
71
73
 
72
- * `string` - `String`
73
- * `text` - `String`
74
- * `integer` - `Integer`
75
- * `number` - `BigDecimal`
76
- * `money` - `BigDecimal`
77
- * `date` - `Date`
78
- * `datetime` - `DateTime`
74
+ * `string` - `Filemaker::Model::Types::Text`
75
+ * `text` - `Filemaker::Model::Types::Text`
76
+ * `integer` - `Filemaker::Model::Types::Integer`
77
+ * `number` - `Filemaker::Model::Types::BigDecimal`
78
+ * `money` - `Filemaker::Model::Types::BigDecimal`
79
+ * `date` - `Filemaker::Model::Types::Date`
80
+ * `datetime` - `Filemaker::Model::Types::Time`
79
81
  * `email` - `Filemaker::Model::Types::Email`
80
- * `object` - `Filemaker::Model::Types::Attachment`
82
+
83
+ You can create your own custom type by providing these 3 class methods:
84
+
85
+ * `__filemaker_cast_to_ruby_object`
86
+ * `__filemaker_serialize_for_update`
87
+ * `__filemaker_serialize_for_query`
88
+
89
+ And register it with:
90
+
91
+ ```ruby
92
+ Filemaker::Model::Type.register(:fast_string, FastStringType)
93
+ ```
81
94
 
82
95
  If the field name has spaces, you can use `fm_name` to identify the real FileMaker field name.
83
96
 
@@ -107,7 +120,6 @@ class Job
107
120
  datetime :created_at
108
121
  datetime :published_at, fm_name: 'ModifiedDate'
109
122
  money :salary
110
- object :attachment
111
123
 
112
124
  validates :title, presence: true
113
125
 
@@ -125,6 +137,8 @@ development:
125
137
  account_name: <%= ENV['FILEMAKER_ACCOUNT_NAME'] %>
126
138
  password: <%= ENV['FILEMAKER_PASSWORD'] %>
127
139
  ssl: true
140
+ ssl_verifypeer: false
141
+ ssl_verifyhost: 0
128
142
  log: curl
129
143
 
130
144
  read_slave:
@@ -18,17 +18,14 @@ Gem::Specification.new do |spec|
18
18
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
19
  spec.require_paths = ['lib']
20
20
 
21
- spec.add_runtime_dependency 'faraday'
22
21
  spec.add_runtime_dependency 'typhoeus'
23
22
  spec.add_runtime_dependency 'nokogiri', '~> 1.7'
24
23
  spec.add_runtime_dependency 'activemodel'
25
24
  spec.add_runtime_dependency 'globalid'
26
- spec.add_runtime_dependency 'mimemagic'
27
- spec.add_runtime_dependency 'mime-types'
28
25
 
29
26
  spec.add_development_dependency 'bundler', '~> 1.13'
30
27
  spec.add_development_dependency 'rake', '~> 12.0'
31
- spec.add_development_dependency 'rspec', '~> 3.5'
28
+ spec.add_development_dependency 'rspec', '~> 3.7'
32
29
  spec.add_development_dependency 'rubocop'
33
30
  spec.add_development_dependency 'pry-byebug'
34
31
  end
@@ -17,39 +17,37 @@ require 'active_support/core_ext'
17
17
  require 'active_model'
18
18
  require 'globalid'
19
19
 
20
+ require 'filemaker/model/type'
20
21
  require 'filemaker/model/criteria'
21
22
  require 'filemaker/model'
22
23
 
23
24
  require 'yaml'
24
25
 
26
+ Typhoeus::Config.user_agent = "filemaker-ruby-#{Filemaker::VERSION}".freeze
27
+
25
28
  module Filemaker
26
29
  module_function
27
30
 
28
31
  # Based on the environment, register the server so we only ever have one
29
32
  # instance of Filemaker::Server per named session. The named session will be
30
33
  # defined at the `filemaker.yml` config file.
31
- def load!(path, environment = nil)
34
+ def load!(path, environment = :development)
32
35
  file_string = ERB.new(File.new(path).read).result
33
36
  sessions = YAML.safe_load(file_string)[environment.to_s]
34
37
  raise Errors::ConfigurationError, 'Environment wrong?' if sessions.nil?
35
38
 
36
39
  sessions.each_pair do |key, value|
37
- registry[key] = Filemaker::Server.new do |config|
38
- config.host = value.fetch('host') do
39
- raise Errors::ConfigurationError, 'Missing config.host'
40
- end
41
-
42
- config.account_name = value.fetch('account_name') do
43
- raise Errors::ConfigurationError, 'Missing config.account_name'
44
- end
45
-
46
- config.password = value.fetch('password') do
47
- raise Errors::ConfigurationError, 'Missing config.password'
48
- end
40
+ registry[key] = Filemaker::Server.new do |c|
41
+ c.host = value['host']
42
+ c.account_name = value['account_name']
43
+ c.password = value['password']
49
44
 
50
- config.ssl = value['ssl'] if value['ssl']
51
- config.log = value['log'] if value['log']
52
- config.endpoint = value['endpoint'] if value['endpoint']
45
+ c.ssl = value['ssl'] if value['ssl']
46
+ c.ssl_verifypeer = value['ssl_verifypeer'] if value['ssl_verifypeer']
47
+ c.ssl_verifyhost = value['ssl_verifyhost'] if value['ssl_verifyhost']
48
+ c.log = value['log'] if value['log']
49
+ c.endpoint = value['endpoint'] if value['endpoint']
50
+ c.timeout = value['timeout'] if value['timeout']
53
51
  end
54
52
  end
55
53
  end
@@ -1,10 +1,15 @@
1
1
  module Filemaker
2
2
  class Configuration
3
- attr_accessor :host, :account_name, :password, :ssl, :endpoint
3
+ attr_accessor :host, :account_name, :password, :endpoint
4
+ attr_accessor :ssl_verifypeer, :ssl_verifyhost, :ssl
5
+ attr_accessor :timeout
4
6
  attr_accessor :log
5
7
 
6
8
  def initialize
7
9
  @endpoint = '/fmi/xml/fmresultset.xml'
10
+ @timeout = 0
11
+ @ssl_verifypeer = false
12
+ @ssl_verifyhost = 0
8
13
  end
9
14
 
10
15
  def not_configurable?
@@ -5,6 +5,7 @@ module Filemaker
5
5
  class ParameterError < StandardError; end
6
6
  class CoerceError < StandardError; end
7
7
  class ConfigurationError < StandardError; end
8
+ class HttpTimeoutError < StandardError; end
8
9
 
9
10
  class FilemakerError < StandardError
10
11
  attr_reader :code
@@ -24,13 +24,12 @@ module Filemaker
24
24
  # @return [Filemaker::Resultset]
25
25
  def perform_request(action, args, options)
26
26
  response, params = server.perform_request(
27
- :post,
28
27
  action,
29
28
  default_params.merge(args),
30
29
  options
31
30
  )
32
31
 
33
- Filemaker::Resultset.new(server, response.body, params)
32
+ Filemaker::Resultset.new(server, response.response_body, params)
34
33
  end
35
34
 
36
35
  private
@@ -38,13 +38,15 @@ module Filemaker
38
38
  value.delete('$,')
39
39
  end
40
40
 
41
- def coerce(value)
41
+ # Raw XML data `inner_text` into Ruby native object as best we can based
42
+ # on its built-in metadata's data_type
43
+ def raw_cast(value)
42
44
  value = value.to_s.strip
43
45
  return nil if value.empty?
44
46
 
45
47
  case data_type
46
48
  when 'number'
47
- BigDecimal.new(remove_decimal_mark(value))
49
+ BigDecimal(remove_decimal_mark(value))
48
50
  when 'date'
49
51
  # date_format likely will be '%m/%d/%Y', but if we got '19/8/2014',
50
52
  # then `strptime` will raise invalid date error
@@ -76,8 +78,8 @@ module Filemaker
76
78
  else
77
79
  value
78
80
  end
79
- rescue StandardError
80
- warn "Could not coerce #{name}: #{value}"
81
+ rescue StandardError => e
82
+ warn "Could not coerce #{name}: #{value} due to #{e.message}"
81
83
  value
82
84
  end
83
85
 
@@ -5,20 +5,22 @@ module Filemaker
5
5
  extend ActiveSupport::Concern
6
6
  include Components
7
7
 
8
- # @return [Boolean] indicates if this is a new fresh record
9
- attr_reader :attributes, :new_record, :record_id, :mod_id, :portals
8
+ attr_reader :new_record, :record_id, :mod_id, :portals
10
9
 
11
10
  included do
12
11
  class_attribute :db, :lay, :registry_name, :server, :api, :per_page
13
12
  self.per_page = Kaminari.config.default_per_page if defined?(Kaminari)
14
13
  end
15
14
 
16
- def initialize(attrs = nil)
15
+ def initialize(attributes = {})
16
+ # We did not manage to use `ActiveModel::AttributeAssignment`
17
+ # super
18
+
17
19
  @new_record = true
18
- @attributes = {}
19
20
  @relations = {}
20
21
  apply_defaults
21
- process_attributes(attrs)
22
+ process_attributes(attributes)
23
+ clear_changes_information
22
24
  end
23
25
 
24
26
  def new_record?
@@ -34,7 +36,7 @@ module Filemaker
34
36
  end
35
37
 
36
38
  def model_key
37
- @model_cache_key ||= self.class.model_name.cache_key
39
+ @model_key ||= self.class.model_name.cache_key
38
40
  end
39
41
 
40
42
  def cache_key
@@ -53,7 +55,7 @@ module Filemaker
53
55
  end
54
56
 
55
57
  def to_param
56
- id.to_s if id
58
+ id&.to_s
57
59
  end
58
60
 
59
61
  def fm_attributes
@@ -65,17 +67,19 @@ module Filemaker
65
67
  changed.each do |attr_name|
66
68
  dirty[attr_name] = attributes[attr_name]
67
69
  end
68
- self.class.with_model_fields(dirty)
70
+
71
+ # We need to use serialize_for_update instead
72
+ self.class.with_model_fields(dirty, use_query: false)
69
73
  end
70
74
 
71
75
  private
72
76
 
73
- def process_attributes(attrs)
74
- attrs ||= {}
75
- return if attrs.empty?
77
+ def process_attributes(attributes)
78
+ return if attributes.empty?
76
79
 
77
- attrs.each_pair do |key, value|
78
- public_send("#{key}=", value) if respond_to?("#{key}=")
80
+ attributes.each_pair do |key, value|
81
+ setter = :"#{key}="
82
+ public_send(setter, value) if respond_to?(setter)
79
83
  end
80
84
  end
81
85
 
@@ -125,7 +129,7 @@ module Filemaker
125
129
  # is an array. Without the test and expectation setup, debugging the
126
130
  # output will take far longer to realise. This reinforce the belief that
127
131
  # TDD is in fact a valuable thing to do.
128
- def with_model_fields(criterion, coerce = true)
132
+ def with_model_fields(criterion, use_query: true)
129
133
  accepted_fields = {}
130
134
 
131
135
  criterion.each_pair do |key, value|
@@ -140,13 +144,20 @@ module Filemaker
140
144
  if value.is_a? Array
141
145
  temp = []
142
146
  value.each do |v|
143
- temp << (coerce ? field.coerce(v) : v)
147
+ temp << if use_query
148
+ field.serialize_for_query(v)
149
+ else
150
+ field.serialize_for_update(v)
151
+ end
144
152
  end
145
153
 
146
154
  accepted_fields[field.fm_name] = temp
147
155
  else
148
- accepted_fields[field.fm_name] = \
149
- coerce ? field.coerce(value) : value
156
+ accepted_fields[field.fm_name] = if use_query
157
+ field.serialize_for_query(value)
158
+ else
159
+ field.serialize_for_update(value)
160
+ end
150
161
  end
151
162
  end
152
163
 
@@ -2,11 +2,22 @@ module Filemaker
2
2
  module Model
3
3
  module Batches
4
4
  def in_batches(batch_size: 200, options: {})
5
+ output = []
6
+ total = self.in(options).count
7
+ pages = (total / batch_size.to_f).ceil
8
+ 1.upto(pages) do |page|
9
+ output.concat self.in(options).per(batch_size).page(page)
10
+ end
11
+
12
+ output
13
+ end
14
+
15
+ def where_batches(batch_size: 200, options: {})
5
16
  output = []
6
17
  total = where(options).count
7
18
  pages = (total / batch_size.to_f).ceil
8
19
  1.upto(pages) do |page|
9
- output.concat where(options).per(batch_size).page(page).all
20
+ output.concat where(options).per(batch_size).page(page)
10
21
  end
11
22
 
12
23
  output
@@ -24,12 +24,17 @@ module Filemaker
24
24
  record.each_key do |fm_field_name|
25
25
  # record.keys are all lowercase
26
26
  field = object.class.find_field_by_name(fm_field_name)
27
+
28
+ # Do not bother with undefined field, we don't necessarily need all
29
+ # FM's fields
27
30
  next unless field
28
31
 
29
- object.attributes[field.name] = field.coerce(
30
- record[fm_field_name],
31
- object.class
32
- )
32
+ setter = :"#{field.name}="
33
+ value = field.cast(record[fm_field_name])
34
+ object.public_send(setter, value)
35
+
36
+ # So after hydrating, we do not say it was dirty
37
+ object.clear_changes_information
33
38
  end
34
39
 
35
40
  object
@@ -15,11 +15,19 @@ module Filemaker
15
15
  extend ActiveModel::Callbacks
16
16
  end
17
17
 
18
+ # Includes Naming, Translation, Validations, Conversion and
19
+ # AttributeAssignment
18
20
  include ActiveModel::Model
21
+
19
22
  include ActiveModel::Dirty
20
23
  include ActiveModel::Serializers::JSON
24
+
25
+ # Provide before/after_validation
21
26
  include ActiveModel::Validations::Callbacks
27
+
28
+ # A global URI good for background job processing
22
29
  include GlobalID::Identification
30
+
23
31
  include Fields
24
32
  include Relations
25
33
  include Persistable
@@ -1,6 +1,3 @@
1
- require 'filemaker/model/types/email'
2
- require 'filemaker/model/types/attachment'
3
-
4
1
  module Filemaker
5
2
  module Model
6
3
  class Field
@@ -9,52 +6,50 @@ module Filemaker
9
6
  def initialize(name, type, options = {})
10
7
  @name = name
11
8
  @type = type
12
- @default_value = coerce(options.fetch(:default) { nil })
9
+ @default_value = serialize_for_update(options.fetch(:default) { nil })
13
10
 
14
11
  # We need to downcase because Filemaker::Record is
15
12
  # HashWithIndifferentAndCaseInsensitiveAccess
16
- @fm_name = (options.fetch(:fm_name) { name }).to_s.downcase
13
+ @fm_name = (options.fetch(:fm_name) { name }).to_s.downcase.freeze
14
+ end
15
+
16
+ # Will delegate to the underlying @type for casting
17
+ # From raw input to Ruby type
18
+ def cast(value)
19
+ return value if skip_modifying_value(value)
20
+ @type.__filemaker_cast_to_ruby_object(value)
21
+ rescue StandardError => e
22
+ warn "[#{e.message}] Could not cast: #{name}=#{value}"
23
+ value
17
24
  end
18
25
 
19
- # From FileMaker to Ruby.
20
- #
21
- # If the value is `==` (match empty) or `=*` (match record), then we will
22
- # skip coercion.
23
- #
24
- # Date and DateTime will be special. If the value is a String, the query
25
- # may be '2016', '3/2016' or '3/24/2016' for example.
26
- def coerce(value, klass = nil)
27
- return nil if value.nil?
28
- return value if value =~ /^==|=\*/
29
- return value if value =~ /(\.\.\.)/
26
+ # Convert to Ruby type situable for making FileMaker update
27
+ # For attr_writer
28
+ def serialize_for_update(value)
29
+ return value if skip_modifying_value(value)
30
+ @type.__filemaker_serialize_for_update(value)
31
+ rescue StandardError => e
32
+ warn "[#{e.message}] Could not serialize for update: #{name}=#{value}"
33
+ value
34
+ end
30
35
 
31
- if @type == String
32
- value.to_s
33
- elsif @type == Integer
34
- value.to_i
35
- elsif @type == BigDecimal
36
- BigDecimal.new(value.to_s)
37
- elsif @type == Date
38
- return value if value.is_a? Date
39
- return value.to_s if value.is_a? String
40
- Date.parse(value.to_s)
41
- elsif @type == DateTime
42
- return value if value.is_a? DateTime
43
- return value.to_s if value.is_a? String
44
- DateTime.parse(value.to_s)
45
- elsif @type == Filemaker::Model::Types::Email
46
- return value if value.is_a? Filemaker::Model::Types::Email
47
- Filemaker::Model::Types::Email.new(value)
48
- elsif @type == Filemaker::Model::Types::Attachment
49
- return value if value.is_a? Filemaker::Model::Types::Attachment
50
- Filemaker::Model::Types::Attachment.new(value, klass)
51
- else
52
- value
53
- end
36
+ # Convert to Ruby type situable for making FileMaker query
37
+ def serialize_for_query(value)
38
+ return value if skip_modifying_value(value)
39
+ @type.__filemaker_serialize_for_query(value)
54
40
  rescue StandardError => e
55
- warn "[#{e.message}] Could not coerce #{name}: #{value}"
41
+ warn "[#{e.message}] Could not serialize for query: #{name}=#{value}"
56
42
  value
57
43
  end
44
+
45
+ # Doc why we skip it!
46
+ # TODO - we may need to customize it for query and update. For example
47
+ # query will bypass `==`, but update do not need to care.
48
+ def skip_modifying_value(value)
49
+ return true if value.nil?
50
+ return true if value =~ /^==|=\*/
51
+ return true if value =~ /(\.\.\.)/
52
+ end
58
53
  end
59
54
  end
60
55
  end