Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Demonstration of Extension Approach 2 #47

Closed
wants to merge 10 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions apps/extensions_manager/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/_build
/cover
/deps
erl_crash.dump
*.ez


# Vim Swap Files
*.swp
/**/*.swp
/**/**/*.swp
/**/**/**/*.swp
/**/**/**/**/*.swp
1 change: 1 addition & 0 deletions apps/extensions_manager/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Extensions Manager
30 changes: 30 additions & 0 deletions apps/extensions_manager/config/config.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# This file is responsible for configuring your application
# and its dependencies with the aid of the Mix.Config module.
use Mix.Config

# This configuration is loaded before any dependency and is restricted
# to this project. If another project depends on this project, this
# file won't be loaded nor affect the parent project. For this reason,
# if you want to provide default values for your application for
# 3rd-party users, it should be done in your "mix.exs" file.

# You can configure for your application as:
#
# config :extensions_manager, key: :value
#
# And access this configuration in your application as:
#
# Application.get_env(:extensions_manager, :key)
#
# Or configure a 3rd-party app:
#
# config :logger, level: :info
#

# It is also possible to import configuration files, relative to this
# directory. For example, you can emulate configuration per environment
# by uncommenting the line below and defining dev.exs, test.exs and such.
# Configuration from the imported file will override the ones defined
# here (which is why it is important to import them last).
#
# import_config "#{Mix.env}.exs"
2 changes: 2 additions & 0 deletions apps/extensions_manager/lib/extensions_manager.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
defmodule ExtensionsManager do
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
defmodule ExtensionsManager.ExtendProduct do
use ExtensionsManager.ModelExtension
use FavoriteProducts.NectarExtension, install: "products"
end

defmodule ExtensionsManager.ExtendUser do
use ExtensionsManager.ModelExtension
use FavoriteProducts.NectarExtension, install: "users"
end

defmodule ExtensionsManager.Router do
use ExtensionsManager.RouterExtension
use FavoriteProducts.NectarExtension, install: "router"
end
75 changes: 75 additions & 0 deletions apps/extensions_manager/lib/extensions_manager/model_extension.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
defmodule ExtensionsManager.ModelExtension do
defmacro __using__(_opts) do
quote do
Module.register_attribute(__MODULE__, :schema_changes, accumulate: true)
Module.register_attribute(__MODULE__, :method_block, accumulate: true)

import ExtensionsManager.ModelExtension, only: [add_to_schema: 4, add_to_schema: 3, include_method: 1]
@before_compile ExtensionsManager.ModelExtension

defmacro __using__(_opts) do

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does defining a __using__/1 prevent the user from defining their own __using__/1? Since most use X statements are at the top, that'd mean this one would match everything and any other __using__/1 definitions would generate a compiler warning and never match.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, Unfortunately this will prevent the another using/1 declaration in ExtensionsManager.ExtendUser (where its currently being used)

ExtensionsManager.ExtendUser is a special module wrapping up schema changes and support functions for Nectar.User (Ecto Model) from other modules (extensions) so we never envisaged a situation where another using macro would be required for ExtensionsManager.ExtendUser

But for exceptional cases, we can probably add a defoverridable [__using__: 1] in ExtensionsManager.ExtendUser trusting the developer's intent for change 💭 💂‍♂️

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm thinking maybe you're attempt to implicitly change the schema definition should take a more explicit approach. Create a macro instead that adds the fields you want and just insert that macro where it needs to go.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mgwidmann Project is in very early stages. It will evolve where Nectar and all other extensions will be phoenix projects which can be downloaded as hex packages.

Nectar and extensions can be grown and develop in isolations. Nectar being maintained by one team and extensions probably by many different teams where no one team will own all the extensions and Nectar Project.

So, Nectar should provide a way to extensions to hook back and grow independently.

Changes in extensions should not result / demand a change in Nectar Project but it could work otherwise, wherein extensions can adapt to change as per Nectar, if need some specific functionality which is provided by Nectar later.

Not very sure, whether above answers your question or not :( 😞

We are in process of releasing the Blog Series which will provide in-depth details of nature of project and how it would be used.

We hope, it should make things more clear 😊

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, its your choice. Just wanted to provide some input. I'll look for your blog post.

quote do
import unquote(__MODULE__), only: [extensions: 0, schema_changes: 0, include_methods: 0, method_blocks: 0]
@before_compile unquote(__MODULE__)
end
end

defmacro __before_compile__(_env) do
quote do
include_methods
end
end
end
end

defmacro add_to_schema(method, name, through) do
quote bind_quoted: [name: name, through: through, method: method], location: :keep do
Module.put_attribute(__MODULE__, :schema_changes, {method, name, through})
end
end
defmacro add_to_schema(method, name, type, options) do
quote bind_quoted: [name: name, type: type, options: options, method: method], location: :keep do
Module.put_attribute(__MODULE__, :schema_changes, {method, name, type, options})
end
end

defmacro include_method(methd) do
block = Macro.escape(methd)
quote bind_quoted: [block: block] do
Module.put_attribute(__MODULE__, :method_block, block)
end
end

defmacro __before_compile__(_env) do
quote do
defmacro extensions do
quote do
Enum.map(schema_changes, fn
({:field, name, type, options}) -> field name, type, options
({:has_one, name, type, options}) -> has_one name, type, options
({:has_many, name, type, options}) -> has_many name, type, options
({:has_many, name, through}) -> has_many name, through
end)
end
end

def schema_changes do
@schema_changes
end

defmacro include_methods do
quote do
unquote(Enum.map(method_blocks, fn (method_block) -> method_block end))

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not just unquote(method_blocks)? The map doesn't look like it makes any effect on the data...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for pointing this out, yes it can be safely removed with no impact. 👍

end
end

def method_blocks do
@method_block
end
end
end
end

defmodule ExtensionsManager.DefaultExtend do
use ExtensionsManager.ModelExtension
end
25 changes: 25 additions & 0 deletions apps/extensions_manager/lib/extensions_manager/router_extension.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
defmodule ExtensionsManager.RouterExtension do
defmacro __using__(_opts) do
quote do
Module.register_attribute(__MODULE__, :defined_routes, accumulate: true)

import ExtensionsManager.RouterExtension, only: [define_route: 1]
@before_compile ExtensionsManager.RouterExtension
end
end

defmacro define_route([do: rt]) do
block = Macro.escape(rt)
quote bind_quoted: [block: block] do
Module.put_attribute(__MODULE__, :defined_routes, block)
end
end

defmacro __before_compile__(_env) do
quote do
defmacro mount do
@defined_routes
end
end
end
end
41 changes: 41 additions & 0 deletions apps/extensions_manager/lib/extensions_manager/sample_macro.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
defmodule ExtensionsManager.SampleMacro do
defmacro __using__(_opts) do
quote do
Module.register_attribute(__MODULE__, :module_blocks, accumulate: true)
import ExtensionsManager.SampleMacro, only: [define_model: 1]
@before_compile ExtensionsManager.SampleMacro

defmacro __using__(_opts) do
quote do
import unquote(__MODULE__), only: [include_models: 0]
@before_compile unquote(__MODULE__)
end
end

defmacro __before_compile__(_env) do
quote do
include_models
end
end
end
end

defmacro __before_compile__(_env) do
quote do
defmacro include_models do
quote do
unquote(Enum.map(module_blocks, fn (module_block) -> module_block end))
end
end
def module_blocks do
@module_blocks
end
end
end

defmacro define_model([do: block]) do
quote bind_quoted: [block: block] do
Module.put_attribute(__MODULE__, :module_blocks, block)
end
end
end
40 changes: 40 additions & 0 deletions apps/extensions_manager/mix.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
defmodule ExtensionsManager.Mixfile do
use Mix.Project

def project do
[app: :extensions_manager,
version: "0.0.1",
build_path: "../../_build",
config_path: "../../config/config.exs",
deps_path: "../../deps",
lockfile: "../../mix.lock",
elixir: "~> 1.2",
build_embedded: Mix.env == :prod,
start_permanent: Mix.env == :prod,
deps: deps]
end

# Configuration for the OTP application
#
# Type "mix help compile.app" for more information
def application do
[applications: [:logger]]
end

# Dependencies can be Hex packages:
#
# {:mydep, "~> 0.3.0"}
#
# Or git/path repositories:
#
# {:mydep, git: "https://github.com/elixir-lang/mydep.git", tag: "0.1.0"}
#
# To depend on another app inside the umbrella:
#
# {:myapp, in_umbrella: true}
#
# Type "mix help deps" for more examples and options
defp deps do
[{:favorite_products, in_umbrella: true}]
end
end
7 changes: 7 additions & 0 deletions apps/extensions_manager/test/extensions_manager_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
defmodule ExtensionsManagerTest do
use ExUnit.Case

test "the truth" do
assert 1 + 1 == 2
end
end
1 change: 1 addition & 0 deletions apps/extensions_manager/test/test_helper.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ExUnit.start()
29 changes: 29 additions & 0 deletions apps/favorite_products/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# App artifacts
/_build
/db
/deps
/*.ez

# Generate on crash by the VM
erl_crash.dump

# The config/prod.secret.exs file by default contains sensitive
# data and you should not commit it into version control.
#
# Alternatively, you may comment the line below and commit the
# secrets file as long as you replace its contents by environment
# variables.
/config/prod.secret.exs
/config/dev.secret.exs
/config/test.secret.exs

# Vim Swap Files
*.swp
/**/*.swp
/**/**/*.swp
/**/**/**/*.swp
/**/**/**/**/*.swp

# Misc
.DS_Store
**/.DS_Store
19 changes: 19 additions & 0 deletions apps/favorite_products/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# FavoriteProducts

To start your Phoenix app:

* Install dependencies with `mix deps.get`
* Create and migrate your database with `mix ecto.create && mix ecto.migrate`
* Start Phoenix endpoint with `mix phoenix.server`

Now you can visit [`localhost:4000`](http://localhost:4000) from your browser.

Ready to run in production? Please [check our deployment guides](http://www.phoenixframework.org/docs/deployment).

## Learn more

* Official website: http://www.phoenixframework.org/
* Guides: http://phoenixframework.org/docs/overview
* Docs: http://hexdocs.pm/phoenix
* Mailing list: http://groups.google.com/group/phoenix-talk
* Source: https://github.com/phoenixframework/phoenix
70 changes: 70 additions & 0 deletions apps/favorite_products/brunch-config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
exports.config = {
// See http://brunch.io/#documentation for docs.
files: {
javascripts: {
joinTo: "js/app.js"

// To use a separate vendor.js bundle, specify two files path
// https://github.com/brunch/brunch/blob/stable/docs/config.md#files
// joinTo: {
// "js/app.js": /^(web\/static\/js)/,
// "js/vendor.js": /^(web\/static\/vendor)|(deps)/
// }
//
// To change the order of concatenation of files, explicitly mention here
// https://github.com/brunch/brunch/tree/master/docs#concatenation
// order: {
// before: [
// "web/static/vendor/js/jquery-2.1.1.js",
// "web/static/vendor/js/bootstrap.min.js"
// ]
// }
},
stylesheets: {
joinTo: "css/app.css"
},
templates: {
joinTo: "js/app.js"
}
},

conventions: {
// This option sets where we should place non-css and non-js assets in.
// By default, we set this to "/web/static/assets". Files in this directory
// will be copied to `paths.public`, which is "priv/static" by default.
assets: /^(web\/static\/assets)/
},

// Phoenix paths configuration
paths: {
// Dependencies and current project directories to watch
watched: [
"web/static",
"test/static"
],

// Where to compile files to
public: "priv/static"
},

// Configure your plugins
plugins: {
babel: {
// Do not use ES6 compiler in vendor code
ignore: [/web\/static\/vendor/]
}
},

modules: {
autoRequire: {
"js/app.js": ["web/static/js/app"]
}
},

npm: {
enabled: true,
// Whitelist the npm deps to be pulled in as front-end assets.
// All other deps in package.json will be excluded from the bundle.
whitelist: ["phoenix", "phoenix_html"]
}
};
Loading